Программно вызвать основную функцию в Windows

У меня есть стороннее консольное приложение. Мне нужно запустить его из моего приложения, но я не могу запустить его как отдельный процесс (потому что мне нужно работать с его зависимостями: вручную заполнять таблицы импорта, настраивать хуки и т. д.). Так что, вероятно, мне следует вручную вызвать функцию main этого исполняемого файла. Вот как я пытаюсь это сделать:

  1. Загрузите этот EXE-файл, используя auto hMod = LoadLibrary("console_app.exe")
  2. Заполните таблицу импорта этого exe вручную
  3. Получите точку входа этого EXE и вызовите его

И я застрял с последним шагом.

Вот как я пытаюсь вызвать точку входа:

void runMain(HINSTANCE hInst)
{
    typedef BOOL(WINAPI *PfnMain)(int, char*[]);

    auto imageNtHeaders = ImageNtHeader(hInst);
    auto pfnMain = (PfnMain)(DWORD_PTR)(imageNtHeaders->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)hInst);

    char* args[] = { R"(<console_app_path>)", R"(arg1)", R"(arg2)" };
    pfnMain(3, args);
}

Оно работает. Но это работает так, как будто аргументов нет.

Где я не прав? Как я могу запустить исполняемый файл внутри моего процесса с аргументами? Спасибо.

ОБНОВИТЬ:

Я исследовал, как мой конкретный сторонний exe получает аргументы cmd, и обнаружил, что:

  1. Он вообще не импортирует GetCommandLine и не вызывает его
  2. После вызова call _initterm аргументы argc и argv доступны через cs:argc и cs:argv (см. рисунки ниже) введите здесь описание изображения введите здесь описание изображения
  3. Аргументы CMD, которые я передаю своему основному консольному приложению, также передаются дочернему EXE.

Не могли бы вы объяснить, что на самом деле делает _initterm и где на самом деле хранятся аргументы CMD?


person Dmitry Katkevich    schedule 05.01.2017    source источник
comment
Фактическая точка входа может быть не самой main, а оболочкой вокруг main. В этом случае параметры сильно зависят от реализации.   -  person StoryTeller - Unslander Monica    schedule 05.01.2017
comment
@StoryTeller «Высоко определенная реализация» здесь должна быть «точно указана в спецификации заголовка PE». Потому что загрузчик приложений Windows, конечно же, должен знать эту спецификацию.   -  person Konrad Rudolph    schedule 05.01.2017
comment
@KonradRudolph - естественно. Однако ОП этого не делает.   -  person StoryTeller - Unslander Monica    schedule 05.01.2017
comment
1) будет работать, только если у ваших EXE есть релокации. что не всегда верно для EXE   -  person RbMm    schedule 05.01.2017
comment
Я думаю, действительно лучшее решение - выполнить свой EXE в отдельном процессе и ввести в него собственный DLL в начальной точке с помощью QueueUserAPC, который устанавливает крючки   -  person RbMm    schedule 05.01.2017
comment
@StoryTeller: Фактическая точка входа должна быть оболочкой вокруг main, потому что точка входа PE имеет неправильную подпись и не может получить командную строку.   -  person MSalters    schedule 05.01.2017
comment
Что делать, если сторонняя программа ожидает версию CRT, отличную от версии вашей основной программы? Что, если сторонняя программа не имеет relocs или не делает предположений о том, где она загружена? Я думаю, что этот подход будет болезненным, если он осуществим. Я думаю, что запускать его в отдельном процессе будет безопаснее и проще, даже если это означает необходимость вводить код в этот процесс, чтобы перехватывать то, что вы хотите.   -  person Adrian McCarthy    schedule 05.01.2017
comment
ваше приложение может импортировать __getmainargs или __wgetmainargs из msvcrt.dll - msdn.microsoft.com/en- us/library/ff770599.aspx также можно использовать _acmdln или _wcmdln - msdn.microsoft.com/en-us/library/ff770586.aspx   -  person RbMm    schedule 06.01.2017
comment
_initterm вызов конструкторов глобальных объектов и инициализация глобальных переменных   -  person RbMm    schedule 06.01.2017
comment
Как вы делаете № 2?   -  person Jason    schedule 25.04.2017
comment
@Jason, вот мой еще один вопрос, где вы можете получить фрагмент кода stackoverflow.com/questions/40606514/   -  person Dmitry Katkevich    schedule 26.04.2017


Ответы (2)


Вы звоните в точку входа приложения, а не int main(int, char**). Возможно, вы уже читали, что точка входа программы на C++ — int main(int, char**), но это всего лишь точка зрения C++.

Перспектива Win32 отличается; точка входа - int (*)(void);. Компоновщик Visual Studio ищет int mainCRTStartup(void); и использует его, если вы не укажете другую точку входа с /ENTRY. Реализация по умолчанию mainCRTStartup вызывает GetCommandLine() для заполнения argv[] перед вызовом main(argc,argv). В mainCRTStartup есть и другие вещи, которые вы, возможно, захотите сделать: запустить глобальные ctors, инициализировать состояние CRT, ...

Конечно, это предполагает, что другая программа была скомпилирована с помощью Visual C++, но на каком бы языке она ни была написана, она должна вызывать GetCommandLine.

Теперь, что касается вашей проблемы, вот интересное наблюдение: GetCommandLine() возвращает указатель доступный для записи. Вы можете перезаписать существующую командную строку. Конечно, если вы управляете таблицами импорта, вам решать, что означает GetCommandLine. (Помните, как обычно есть варианты A и W).

Одно предупреждение: MSVCRT не предназначен для двойной инициализации ни статической версии, ни версии DLL. Так что, практически говоря, вы не можете его использовать, и это будет больно.

[edit] В вашем обновлении показан звонок _initterm. Это функция MSVCRT, как я уже намекал. Конкретно,

/***
*crtexe.c - Initialization for console EXE using CRT DLL
*
*       Copyright (c) Microsoft Corporation. All rights reserved.
*
...
/*
 * routine in DLL to do initialization (in this case, C++ constructors)
 */
extern int __cdecl _initterm_e(_PIFV *, _PIFV *);
extern void __cdecl _initterm(_PVFV *, _PVFV *);

DLL MSVCRT вызывает GetCommandLine() от имени EXE.

person MSalters    schedule 05.01.2017
comment
да, мы можем перезаписать существующую командную строку, но новая командная строка не должна быть длиннее существующей - person RbMm; 05.01.2017
comment
MSVCRT не предназначен для двойной инициализации ни статической версии, ни версии DLL. - и что ? DLL однажды загружается в процесс, и многие другие библиотеки DLL и exe могут совместно использовать ее. статический CRT залинкован на одну DLL и тоже будет инициализирован только один раз - так что это ни о чем - person RbMm; 05.01.2017
comment
@RbMm: Проблема в том, что если программа-загрузчик и загруженная программа содержат свою собственную копию mainCRTStartup, то mainCRTStartup будет вызываться дважды. Оба ожидают, что они первыми инициализируют ЭЛТ. DLL не содержит mainCRTStartup; его код запуска должен сначала искать инициализированный CRT. - person MSalters; 05.01.2017
comment
вы ошибаетесь. разные mainCRTStartup в разных модулях. с разными глобальными переменными! дважды не звонил. но две разные функции называются - person RbMm; 05.01.2017
comment
@RbMm: будет только один набор переменных CRT, если оба исполняемых файла загружают DLL CRT. Но это еще не все: код CRT также вызывает ExitProcess после возврата main. Очевидно, это будет больно, потому что загруженное приложение убьет весь процесс. - person MSalters; 06.01.2017
comment
один набор переменных CRT в msvcrt.dll - и этот набор будет инициализирован только один раз, когда msvcrt.dll (или другие DLL CRT) будут загружены в процессе. и многие другие модули в процессе могут совместно использовать его - здесь нет конфликта. про ExitProcess после возвращения main - это правда - person RbMm; 06.01.2017
comment
@RbMm: почитайте исходники ЭЛТ; они в C:\Program Files (x86)\Microsoft Visual Studio XX.0\VC\crt\src. Кроме того, прочитайте правила о том, что вы можете делать, когда DLL загружается в процесс, в частности, о блокировке загрузчика. DllMain в CRT DLL не может этого сделать, потому что он запускается до точки входа исполняемого файла (!!) - person MSalters; 06.01.2017
comment
вы полностью ошибаетесь. вы путаете инициализацию CRT DLL, которая происходит при загрузке DLL в процесс и инициализацию EXE, которая использовала эту CRT DLL - person RbMm; 06.01.2017
comment
в CRT DLL — глобальные переменные для каждого процесса. он инициализировался один раз в DLL_PROCESS_ATACH OP, а не повторно инициализировал CRT DLLs - так что здесь нет никаких проблем. с другой стороны [w]mainCRTStartup - иметь глобальные переменные для каждого модуля - это не конфликтует при самоинициализации с другими модулями. и он использовал CRT, который может быть статически или динамически (это случай OP) связан с ним. снова не путайте CRT api и vars с небольшим PE (dll или exe-заглушкой), который использовал CRT - person RbMm; 06.01.2017

точка входа исполняемого файла (EP) не имеет аргументов - поэтому вы и не можете напрямую вызвать ее с аргументами.

обычное приложение получает аргументы путем разбора командной строки. [w]mainCRTStartup сделайте это - если у вас есть консольное приложение, связанное со средой выполнения c/c++ - это реально EP .

поэтому, если вы Fill Import table of this exe manually, установите исключение для GetCommandLineA и GetCommandLineW функции - перенаправьте его на самостоятельную реализацию и верните свою пользовательскую командную строку.

но если приложение использует нестатическую ссылку CRT, оно может импортировать __getmainargs или __wgetmainargs или даже _acmdln или _wcmdln from msvcrt.dll - так что задача уже становится сложной.

и вы предполагаете, что relocs завершается в EXE, вы не обрабатываете TLS, если он существует, вы не обрабатываете манифест приложения, возможные перенаправления dl и т. д.

но я не могу запустить его как отдельный процесс

это неправда. вы можете и должны запускать его как отдельный процесс - это лучшее решение.

запустите свое приложение с помощью CreateProcess с флагом CREATE_SUSPENDED. здесь вы можете легко установить любую командную строку, которая вам нужна. вам не нужно вручную и не полностью правильно загружать EXE, но система сделает эту задачу за вас.

после создания процесса вам нужно внедрить в него себя DLL, используя QueueUserAPC (но не CreateRemoteThread !!) и, наконец, вызовите ResumeThread

в результате ваш DLL будет загружен и выполнен в первом потоке EXE, как раз перед приложением EP - и здесь вы можете выполнять все необходимые задачи

person RbMm    schedule 05.01.2017