14 июля 2009 г.

Этот проблемный CreateProcess...

Вроде бы эта функция не слишком сложна для понимания, но слишком уж часто её используют неверно или не понимают.

По крайней мере, регулярно появляются вопросы типа этого или этого.

Во-первых, не так сложно посмотреть описание параметров CreateProcess:

...

lpApplicationName [in, optional]
The name of the module to be executed. This module can be a Windows-based application. It can be some other type of module (for example, MS-DOS or OS/2) if the appropriate subsystem is available on the local computer.

The string can specify the full path and file name of the module to execute or it can specify a partial name. If it is a partial name, the function uses the current drive and current directory to complete the specification. The function does not use the search path. This parameter must include the file name extension; no default extension is assumed.

The lpApplicationName parameter can be NULL, and the module name must be the first white space–delimited token in the lpCommandLine string. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin; otherwise, the file name is ambiguous.



lpCommandLine [in, out, optional]
The command line to be executed. The maximum length of this string is 1024 characters. If lpApplicationName is NULL, the module name portion of lpCommandLine is limited to MAX_PATH characters.

The function can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.

The lpCommandLine parameter can be NULL, and the function uses the string pointed to by lpApplicationName as the command line.

If both lpApplicationName and lpCommandLine are non-NULL, *lpApplicationName specifies the module to execute, and *lpCommandLine specifies the command line. The new process can use GetCommandLine to retrieve the entire command line. Console processes written in C can use the argc and argv arguments to parse the command line. Because argv[0] is the module name, C programmers typically repeat the module name as the first token in the command line.

If lpApplicationName is NULL, the first white space–delimited token of the command line specifies the module name. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin (see the explanation for the lpApplicationName parameter). If the file name does not contain an extension, .exe is appended. Therefore, if the file name extension is .com, this parameter must include the .com extension. If the file name ends in a period with no extension, or if the file name contains a path, .exe is not appended.

...
(выделение моё, описание приведено не полностью)

Не понимаете английского? Как вы вообще тогда можете быть программистом? Не будьте беспомощны! Вы можете воспользоваться любым авто-переводчиком:

...
lpApplicationName [In, необязательный]
Имя модуля для запуска. Этот модуль может быть Windows-приложения. Она может быть несколько иной вид модуля (например, MS-DOS или OS / 2), если соответствующие подсистемы имеется на локальном компьютере.

Строка может указать полный путь и имя файла модуля для выполнения или он может уточнить частичное название. Если это часть имени, то функция использует текущий диск и текущий каталог, в полной спецификации. Эта функция не используется путь поиска. Этот параметр должен содержать расширение имени файла; нет умолчанию предполагается продление.

Параметр lpApplicationName может быть NULL, тогда имя модуля имя должно быть первым токеном, отделённым пробелом, в lpCommandLine. Если вы используете длинные названия файла, который содержит в пространстве, использование цитирует строки для указания, где имя файла заканчивается, и начинаются аргументы, в противном случае имя файла является двусмысленным.



lpCommandLine [In, Out, необязательный]
Командная строка для выполнения. Максимальная длина этой строки 1024 символов. Если lpApplicationName является NULL, модуль имя часть lpCommandLine ограничена MAX_PATH символов.

Эта функция может модифицировать содержимое этой строки. Таким образом, этот параметр не может быть указателем для чтения памяти (например, Const переменной или буквальном строка). Если этот параметр является постоянной строки, функция может вызвать нарушение прав доступа.

В lpCommandLine параметр может быть NULL, и функция использует строку отметили в lpApplicationName как из командной строки.

Если оба lpApplicationName и lpCommandLine являются не-NULL, lpApplicationName определяет модуль для выполнения, а lpCommandLine указывает командную строку. Новый процесс может использовать GetCommandLine получить всю командную строку. Консоль процессов написаны на C можно использовать argc и argv для разбора аргументов командной строки. Поскольку argv [0] является именем модуля, C программистов, как правило, повторяют имя модуля как первый знак в командной строке.

Если lpApplicationName равно NULL, первый токен до пробела в командной строке должен быть именем модуля для запуска. Если вы используете длинные названия файла, который содержит в пространстве, использование цитирует строки для указания, где имя файла заканчивается, и начинаются аргументы (см. пояснения к lpApplicationName параметров). Если имя файла не содержит расширения,. EXE прилагается. Поэтому, если в имени файла расширение. COM, этот параметр должен содержать. Ком продления. Если имя файла заканчивается в срок, не расширение, или если имя файла содержит путь. EXE не добавляется.
...

Это не так сложно сделать и так же не сложно понять.

Тем не менее, постоянно встречаются ошибки:
  • Забываем про кавычки или ставим лишние. Кавычки нужны в командной строке (второй параметр CreateProcess) и не нужны в имени модуля (первый параметр).
  • Забываем про имя модуля в командной строке, если явно указали запускаемый модуль (не забываем: командная строка передаётся процессу "как есть").
  • Перемешиваем вообще всё, что возможно перемешать, пихая параметры командной строки в имя модуля.
Если указывается первый параметр, то приложение должно быть указано дважды: первый раз в первом параметре, второй раз - в командной строке (второй параметр).

Логика такая: командная строчка (второй параметр) передаётся программе "как есть". Программа может парсить командную строчку, как ей будет угодно. Соответственно, если в командной строке вы не укажете саму программу, а только её параметры, то сама программа в них запутается: она примет первый параметр за своё имя (нет, ParamStr(0) в Delphi вызывает GetModuleFileName, но другие могут), а второй параметр (если он есть) - за первый (тут ошибётся и Delphi программа тоже).

Имя же программы для запуска загрузчиком ОС берётся из первого параметра, если он задан. Или пытается извлечься из второго, если нет.

Вот это "пытается извлечься" и есть причина, почему всегда рекомендуется указывать первый параметр:
  Application := 'C:\Program Files\MySoft\MyApp.exe';
  Params      := '-n:6 /p5 "C:\Program Files\MySoft\Data.bin"';
  CmdLine     := Format('"%s" %s', [Application, Params]);
  CreateProcess(PChar(Application), PChar(CmdLine), ...);
Так ошибок не будет никогда (обратите внимание на расстановку кавычек и пробелов). А если вы его не укажете - у вашей программы могут быть серьёзные проблемы с безопасностью. Особенно, если вы не используете кавычки.

Связано это с правилами поиска/автодополнения файлов и библиотек для запуска. Не буду тут особенно мыслью растекаться - и сами можете почитать у Рихтера или в MSDN.

Да, кстати, получаете EAccessViolation ("Access violation at address...") при вызове CreateProcess на Delphi 2009? Внимательнее читайте описание функции:
lpCommandLine [in, out, optional]
...
The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.


P.S. Кстати, у меня в блоге когда-то была задачка про CreateProcess.

P.P.S. Да, сорри за задержку в переводах, но дел сейчас невпроворот.

[Добавлено 05.0.3.2014]: больше материала по CreateProcess.

13 комментариев :

  1. Не могли бы вы описать не менее проблемный CreateProcessAsUser? Если по CreateProcess еще встречаются нормальные топики, то ни одного вменяемого примера CreateProcessAsUser я не нашел. Есть правда статья с вариантом на C, и даже частично присутствует ее перевод на Delphi в http://www.delphikingdom.com/asp/answer.asp?IDAnswer=1899 , но видимо сообществу не везет:( Может вы нам подскажите?

    ОтветитьУдалить
  2. Использование CreateProcessAsUser полностью эквивалентно CreateProcess. У неё есть всего один дополнительный параметр - токен пользователя. Все остальные параметры полностью тождественны.

    Единственная проблема, которая может тут возникнуть - у пользователя, идентифицируемого токеном, должен быть доступ к запускаемому файлу. Т.е. как минимум это означает никаких subst- и map- дисков.

    Вот и вся разница.

    ОтветитьУдалить
  3. Ну я бы не сказал что так все просто, слишком много подводных камней. Я так и не нашел работающего примера для запуска из службы (запущенной под System)

    ОтветитьУдалить
  4. Только эти проблемы к CreateProcessAsUser не имеет отношения.

    ОтветитьУдалить
  5. QUOTE:
    "Не могли бы вы описать не менее проблемный CreateProcessAsUser?" - не может он... мыслей своих у него нет... вот на перевод ума только и хватает :)

    ОтветитьУдалить
  6. Я и сам хренею от того, какие "светочи мысли" меня посещают.

    ОтветитьУдалить
  7. А все-таки, для глупых, чем чреват такой вызов?
    CreateProcess(nil, PChar(Format('"%s" %s', [AppName, CmdLine])), ...)
    Т.е. lpApplicationName - nil, а имя приложения всегда обрамляется кавычками

    ОтветитьУдалить
  8. Кроме некоторой своей бессмысленности - ничем. С этим кодом всё в порядке.

    Почему бессмысленности? Ну если у вас есть AppName - чего бы его не указать функции-то? Это "бесплатно" и защитит вас от ошибок.

    Если же AppName нет и у вас на руках только командная строка, то указанный вариант - собственно, единственно верный, т.к. в ApplicationName передавать просто нечего. Но же обычно это означает, что командная строка приходит в программу извне. Т.е. в случае проблем - это не будет багом в вашем коде.

    Я так агрессивно в это тыкаю, потому что меня достал этот баг в известных программах, типа Delphi или Total Commander (автора которой мне долго пришлось убеждать в том, что в его программе - баг, а отсутствие коротких путей на диске не может служить оправданием).

    ОтветитьУдалить
  9. > Почему бессмысленности? Ну если у вас есть AppName - чего бы его не указать функции-то?
    Потому, что дублирование информации в любом виде есть зло.

    Вообще, положа руку на сердце, имхо, здесь не права Microsoft. Ведь в том же ShellExecute они сделали правильно. Приложение отдельно, параметры отдельно. А внутри уже функция должна сформировать ту строку, которая удовлетворяет требованию ОС

    ОтветитьУдалить
  10. > Потому, что дублирование информации в любом виде есть зло.

    Это верно.

    Но конкретно здесь дублирование - меньшее зло. Оно противопоставлено большему злу - необходимости парсить командную строку, если приложение вы не укажете.

    Дублирование информации в данном случае не несёт накладных расходов (потому что она у вас и так уже есть), но если вы имя не передаёте, то накладные расходы будут весьма существенны - это разбор командной строки: не самая быстрая операция.

    По поводу не удачного дизайна CreateProcess - тоже верно, но это обусловлено историческими причинами.

    ОтветитьУдалить
  11. Большое спасибо. Толково, доступно и полезно.

    ОтветитьУдалить
  12. Антон
    >А все-таки, для глупых, чем чреват такой вызов?
    >CreateProcess(nil, PChar(Format('"%s" %s', [AppName, CmdLine])), ...)
    ...
    Александр Алексеев
    > Кроме некоторой своей бессмысленности - ничем.


    Позволю себе уточнить ещё один важный момент -- так, как в этом примере писать нельзя, потому что второй параметр (если он конечно не nil) не только входной, но и выходной (функция может модифицировать эту строку в некоторых случаях). И стало быть передавать вторым параметром указатель на выражение никак нельзя, в лучшем случае получится access violation, в худшем -- плавающий баг. Нужно писать именно как указал Александр -- завести переменную для командной строки и уже её адрес передавать в функцию.

    ОтветитьУдалить
  13. Понимаю, что это некротопик, и пример не на Дельфях, но раз ссылки сюда всплывают в Гугле, то вот пример работы с CreateProcessAsUser (как аноним просил).
    Код работает как раз в службе, запущенной от имени LocalSystem, и запускает некий процесс от имени текущего залогоненного пользователя (поскольку это аппликуха с GUI и должна была взаимодействовать с пользователем).

    Функция, получающая токен текущего залогоненного юзера:
    HANDLE GetCurrentUserTokenHandle(DWORD* pError)
    {
    HANDLE currentToken = 0;
    HANDLE primaryToken = 0;

    int dwSessionId = 0;
    HANDLE hUserToken = 0;
    HANDLE hTokenDup = 0;

    PWTS_SESSION_INFO pSessionInfo = 0;
    DWORD dwCount = 0;

    // Get the list of all terminal sessions
    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1,
    &pSessionInfo, &dwCount);

    int dataSize = sizeof(WTS_SESSION_INFO);
    bool bActiveSession = false;
    // look over obtained list in search of the active session
    for (DWORD i = 0; i < dwCount; ++i)
    {
    WTS_SESSION_INFO si = pSessionInfo[i];
    if (WTSActive == si.State)
    {
    // If the current session is active – store its ID
    dwSessionId = si.SessionId;
    bActiveSession = true;
    break;
    }
    }

    WTSFreeMemory(pSessionInfo);

    // Get token of the logged in user by the active session ID
    BOOL bRet = WTSQueryUserToken(dwSessionId, &currentToken);
    if (bRet == false)
    {
    if (pError)
    *pError = GetLastError();
    return 0;
    }

    //
    bRet = DuplicateTokenEx(currentToken,
    TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
    0, SecurityImpersonation, TokenPrimary, &primaryToken);
    if (bRet == false)
    {
    return 0;
    }

    CloseHandle(currentToken);

    return primaryToken;
    }

    Далее сам кусок кода, запускающий ехе-шник от имени этого юзера:
    ..................
    HANDLE hCurUser = GetCurrentUserTokenHandle(&err);
    if (hCurUser)
    {
    // запускаем с правами юзера (возможно, пониженными), чтобы можно было вывести сообщение в GUI
    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

    SECURITY_ATTRIBUTES sa;
    sa.bInheritHandle = FALSE;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL;

    STARTUPINFO si;
    ZeroMemory(&si, sizeof(STARTUPINFO));
    si.cb = sizeof(STARTUPINFO);
    si.lpDesktop = _T("winsta0\\default");

    wchar_t lpCommandLine[1024];// потому что CreateProcessAsUserW ТРЕБУЕТ буфер
    wcscpy(lpCommandLine, L"\"c:\\windows\\notepad.exe\" \"c:\\readme.txt\""); // тут просто блокнот, для примера
    BOOL bRes = CreateProcessAsUser(
    hCurUser,
    NULL,//_In_opt_ LPCTSTR lpApplicationName,
    lpCommandLine, //_Inout_opt_ LPTSTR lpCommandLine,
    &sa,//_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
    &sa,//_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    false,//_In_ BOOL bInheritHandles,
    0,//_In_ DWORD dwCreationFlags,
    NULL,//_In_opt_ LPVOID lpEnvironment,
    NULL,//_In_opt_ LPCTSTR lpCurrentDirectory,
    &si,//_In_ LPSTARTUPINFO lpStartupInfo,
    &pi//_Out_ LPPROCESS_INFORMATION lpProcessInformation
    );
    if (bRes) {/* хорошо */}
    else {/* плохо */}
    ..................

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.