Вопрос или проблема
Код main.cpp:
#include <iostream>
int main()
{
std::cout << "Hello World!\n";
}
Дизассемблирование в Ghidra:
*************************************************************
* FUNCTION
*************************************************************
int __cdecl main (int _Argc , char * * _Argv , char * * _E
assume GS_OFFSET = 0xff00000000
int EAX:4 <RETURN>
int ECX:4 _Argc
char * * RDX:8 _Argv
char * * R8:8 _Env
undefined1 Stack[-0x10]:1 local_10 XREF[1]:
140012292 (*)
undefined1 Stack[-0xd8]:1 local_d8 XREF[1]: 14001226a (*)
main XREF[1]: main:1400112cb (T) ,
main:1400112cb (j)
140012260 40 55 PUSH RBP
140012262 57 PUSH RDI
140012263 48 81 ec SUB RSP ,0xe8
e8 00 00 00
14001226a 48 8d 6c LEA RBP =>local_d8 ,[RSP + 0x20 ]
24 20
14001226f 48 8d 0d LEA _Argc ,[__6AFE2A9E_TestApplication@cpp ] = 01h
f0 0d 01 00
140012276 e8 63 f1 CALL __CheckForDebuggerJustMyCode void __CheckForDebuggerJustMyCod
ff ff
14001227b 90 NOP
14001227c 48 8d 15 LEA _Argv ,[s_Hello_World!_ ] = "Hello World!\n"
a5 89 00 00
140012283 48 8b 0d MOV _Argc ,qword ptr [->MSVCP140D.DLL::std::cout ] = 00021d22
0e ef 00 00
14001228a e8 f8 ed CALL std::operator<<<> basic_ostream<char,std::char_tra
ff ff
14001228f 90 NOP
140012290 33 c0 XOR EAX ,EAX
140012292 48 8d a5 LEA RSP =>local_10 ,[RBP + 0xc8 ]
c8 00 00 00
140012299 5f POP RDI
14001229a 5d POP RBP
14001229b c3 RET
Пытаюсь понять эту очень простую программу, которую я написал, чтобы попрактиковаться в дизассемблировании в Ghidra.
Пролог мне понятен. Предыдущее RBP
помещается в стек, RSP
уменьшается на 0xE8
(232 байта) для выделения текущего стекового фрейма.
Адрес RSP+0x20
сохраняется в RBP+0xD8
в стеке, но затем я теряю нить рассуждений…
Адрес загружается в _Argc
… Адрес "Hello, World!\n"
загружается в _Argv
, который является char**
, т.е. указателем на массив символов… затем в _Argc
(количество аргументов), которая является целым числом или переменной в 4 байта, загружается указатель на данные слова (8 байт), поскольку это 64-битное приложение, после чего (в большей или меньшей степени) вызывается std::cout
.
Почему кажется, что локальные переменные _Argc
и _Argv
были переименованы для хранения других локальных переменных, одна из которых имеет другой и больший тип данных? Я совсем не это имел в виду? Я не могу это понять, и моя единственная гипотеза заключается в том, что это какое-то волшебство оптимизации компилятора.
Мне это кажется логичным, просто размышляя, что std::cout
в зависимости от своего соглашения о вызовах ожидает массив символов в RDX
(_Argv
), который содержит строки для вывода. Но также использует ECX
(_Argc
), чтобы хранить количество строк, которые нужно вывести?
Я могу быть совершенно не прав. Просто пытаюсь разобраться в ассемблере, сгенерированном Visual Studio 2022. Буду признателен за любую помощь или понимание. Мои попытки найти информацию в Google оказались бесполезными.
Может ли быть просто случайностью, что дизассемблер использует _Argc
и _Argv
вместо ECX
и RDX
, потому что эти регистры использовались для передачи соответствующих аргументов в текущую функцию? Спасибо за любую помощь.
Это плохое вводящее в заблуждение дизассемблирование.
RCX и RDX являются первыми двумя регистрами для передачи аргументов в Windows x64, поэтому аргументы для любого вызова функции должны находиться там. (Кроме случаев, когда аргументы являются числами с плавающей запятой).
MOV _Argc ,qword ptr [->MSVCP140D.DLL::std::cout ]
на самом деле это MOV RCX, ...
, а не ECX, поэтому называть это _Argc
ещё более вводит в заблуждение.
Первый байт машинного кода – 48
(шестнадцатерично), поэтому старший бит наименьшего нибла установлен. Это бит W(idth), который означает размер операнда 64 бита, поэтому он записывает RCX, а не просто ECX. Также мы можем это видеть из qword ptr
в операнде памяти источника.
Нормально, что компиляторы используют входной регистр аргумента для различных целей в течение жизни функции, поэтому заменить имена этих регистров на _Argc
и _Argv
в общем смысле очень не полезно.
Например, переменная длины сдвига будет нуждаться в использовании CL для количества; если бы у вас был код int main(int argc, char *argv[]){ return argc << atoi(argv[1]); }
, вы бы видели, как он перемещает значение возврата atoi в ECX, перегружает или копирует argc
из того места, куда он его положил в процессе вызова atoi
, и выполняет shl eax, cl
. (https://godbolt.org/z/4MnfPG6n6)
Это не повторное использование пространства. _Argc
– это псевдоним для rcx
, а _Argv
– это псевдоним для rdx
. Обратите внимание на инструкции LEA
— целью LEA
всегда является регистр.
Ответ или решение
Компилятор может создавать термины, такие как _Argc
и _Argv
, для обозначения регистров, в которых передаются параметры в функции. В вашем случае, для функций, работающих в архитектуре x64 Windows, используются регистры RCX
и RDX
для передачи первых двух аргументов.
Когда вы смотрите на дизассемблированный код, важно помнить о следующих моментах:
-
Регистры в x64: В x64 архитектуре, при вызове функции, первые аргументы передаются через регистры, и в случае вашей функции
main
, стандартный способ передачи аргументов — это использованиеRCX
дляargc
иRDX
дляargv
. -
Переименование регистров: В дизассемблере Ghidra представление регистров как
_Argc
и_Argv
является просто иллюстрацией для удобства понимания, но не передает полной картины. На самом деле это не совсем корректно, так какMOV
операции записывают значения именно в регистры CPU:MOV _Argc, ...
фактически означаетMOV RCX, ...
. Таким образом, когда программа выполняется, эти регистры используются для временного хранения локальных переменных. -
Использование регистров после передачи аргументов: Компилятор может использовать регистры, которые изначально предназначены для передачи аргументов, для хранения других временных данных по ходу выполнения функции. Это обычная практика оптимизации. Например, если нужно выполнить некоторое вычисление или вызвать другую функцию, компилятор может использовать эти регистры, внезапно "заменяя" их оригинальные значения, но это не означает, что стиль или назначение аргументов изменяются.
-
LEA инструкции: Слово
LEA
(Load Effective Address) используется для загрузки адреса в регистр. Это означает, что_Argc
и_Argv
ссылаются прямо на регистрыRCX
иRDX
, а не на локальные переменные браузерного уровня. Это можно подтвердить, взглянув на соответствующие инструкции и структуру регистров. -
Оптимизация: Некоторые оптимизации могут привести к тому, что компилятор использует один и тот же регистр для различных нужд в пределах одной функции. Однако, как только функция возвращает значение, регистры восстанавливаются, и переданные аргументы будут доступны для любых операций, которые должны выполняться с аргументами.
В заключение, хотя название _Argc
и _Argv
может вводить в заблуждение, это просто замена для более удобного понимания кода. На самом деле, вы все правильно интерпретируете: это не "реподназначение" переменных, а просто использование регистров для временного хранения различных данных согласно правилам оптимизации компилятора.