#winapi #assembly
#winapi #сборка
Вопрос:
Я заметил, что многие примеры языка ассемблера, созданные с использованием прямых вызовов Win32 (без зависимости от времени выполнения C), иллюстрируют использование явного вызова ExitProcess() для завершения программы в конце кода точки входа. Я не говорю об использовании ExitProcess() для выхода из некоторого вложенного местоположения в программе. На удивление меньше примеров, когда код точки входа просто завершается инструкцией RET. Один из примеров, который приходит на ум, — это знаменитый TinyPE, где варианты программы завершаются с помощью инструкции RET, потому что инструкция RET — это один байт. Использование либо ExitProcess(), либо RET, похоже, выполняют свою работу.
RET из точки входа исполняемого файла возвращает значение EAX обратно в загрузчик Windows в KERNEL32, который в конечном итоге передает код выхода обратно в NtTerminateProcess(), по крайней мере, в Windows 7. В Windows XP, я думаю, я помню, что видел, что ExitProcess() даже вызывался непосредственно в конце цепочки очистки потока.
Поскольку в языке ассемблера есть много уважаемых оптимизаций, которые выбираются исключительно для генерации меньшего кода, мне интересно, почему больше кода, плавающего вокруг, предпочитает явный вызов ExitProcess(), а не RET. Это привычка или есть другая причина?
В чистом виде, не будет ли инструкция RET предпочтительнее прямого вызова ExitProcess() ? Прямой вызов ExitProcess() кажется похожим на выход из вашей программы путем ее удаления из диспетчера задач, поскольку это прерывает нормальный поток возврата туда, где загрузчик Windows вызвал вашу точку входа, и, таким образом, пропускает различные операции очистки потоков?
Кажется, я не могу найти никакой информации, относящейся к этой проблеме, поэтому я надеялся, что кто-нибудь сможет пролить свет на эту тему.
Комментарии:
1. Вы уверены, что эти программы имеют свой код в качестве точки входа PE, а не среды выполнения C? Несмотря на это, IIRC всегда есть ExitProcess, ожидающий в стеке над точкой входа исполняемого файла (судя по тому, что я видел из трассировок стека Windows; я не уверен, что это полностью правильно).
2. Это униксизм, проникший в Windows больше всего из-за языка C. Контрпримером является .NET, процесс продолжает выполняться до тех пор, пока в нем есть какие-либо не фоновые потоки.
3. Да, я говорю о программе на чистом языке ассемблера без зависимости от CRT. В случае использования библиотеки времени выполнения C, я полагаю, вы всегда захотите вернуться из main или WinMain с помощью RET, чтобы разрешить _mainCRTStartup /_WinMainCRTStartup восстановить контроль, чтобы можно было выполнить надлежащую очистку.
Ответ №1:
Если ваша основная функция вызывается из библиотеки времени выполнения C, то выход приведет к вызову ExitProcess(), и процесс завершится.
Если ваша основная функция вызывается непосредственно Windows, как это вполне может быть в случае с ассемблерным кодом, то выход приведет только к завершению потока. Процесс завершится тогда и только тогда, когда других потоков не будет. В наши дни это проблема, потому что, даже если вы не создавали никаких потоков, Windows могла создать один или несколько от вашего имени.
Насколько я знаю, это поведение не задокументировано должным образом, но описано в сообщении в блоге Раймонда Чена: «Если вы возвращаетесь из основного потока, завершается ли процесс?».
(Я также сам тестировал это как на Windows 7, так и на Windows 10 и подтвердил, что они вели себя так, как описывает Raymond.)
Дополнение: в последних версиях Windows 10 загрузчик процесса сам по себе является многопоточным, поэтому при первом запуске процесса всегда будут присутствовать дополнительные потоки.
Комментарии:
1. @PeterCordes: моя тестовая программа вернулась из функции main. Это не привело к завершению процесса.
2. Используя 64-разрядную версию Win7 и 32-разрядную чистую asm-программу, после точки входа RET я вернулся к неназванной точке в KERNEL32, вложенной из ntdll.___RtlUserThreadStart. Следующим ВЫЗОВОМ был ntdll.RtlExitUserThread , который вызвал ntdll.RtlExitUserProcess, который вызвал ntdll. NtTerminateProcess, который выполнил «ВЫЗОВ DWORD PTR FS: [0C0]», который вышел из процесса после возврата. При соединении с MSVCRT 7.1: как только main() RET возвращается к _mainCRTStartup, это приводит к MSVCR71.exit -> MSVCR71.doexit -> KERNEL32.ExitProcess, который, в свою очередь, вызывает ntdll.RtlExitUserProcess, и это выдает то же самое, что и выше.
3. @PeterCordes: В качестве дополнения к комментарию выше я не заметил, чтобы загрузчик Windows искусственно помещал адрес ExitProcess в стек ни в тестах pure-asm, ни в CRT. Мне это интересно, потому что об этом упоминали несколько человек. Если это действительно произошло, я думаю, что это должна была быть более старая версия Windows? Кто-нибудь помнит, какой именно?
4. @PeterCordes: сеанс отладки byteptr показывает, что
RtlExitUserThread
вызывается, что согласуется с сообщением Раймонда и моими собственными наблюдениями. Кажется безопасным предположить, чтоRtlExitUserThread
вызовы выполняются толькоRtlExitUserProcess
тогда, когда завершающий поток является последним в процессе. (Я бы наивно ожидал, что это произойдет в ядре, но теперь, когда я думаю об этом, это должно происходить в пользовательском режиме, чтобы поддерживать DllMain.) Если позволит время, я перепроверю это сам, когда вернусь завтра в офис.5. Чтобы немного прояснить это, хотя это выглядит так, как будто ntdll.RtlExitUserProcess будет вызываться в любом случае, это не так, потому что вызов ntdll. NtTerminateThread никогда не возвращается, т. Е. Поток завершает сам себя.