Почему кажется, что компилятор переиспользует Argc и Argv в моей функции?

Вопрос или проблема

Код 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 для передачи первых двух аргументов.

Когда вы смотрите на дизассемблированный код, важно помнить о следующих моментах:

  1. Регистры в x64: В x64 архитектуре, при вызове функции, первые аргументы передаются через регистры, и в случае вашей функции main, стандартный способ передачи аргументов — это использование RCX для argc и RDX для argv.

  2. Переименование регистров: В дизассемблере Ghidra представление регистров как _Argc и _Argv является просто иллюстрацией для удобства понимания, но не передает полной картины. На самом деле это не совсем корректно, так как MOV операции записывают значения именно в регистры CPU: MOV _Argc, ... фактически означает MOV RCX, .... Таким образом, когда программа выполняется, эти регистры используются для временного хранения локальных переменных.

  3. Использование регистров после передачи аргументов: Компилятор может использовать регистры, которые изначально предназначены для передачи аргументов, для хранения других временных данных по ходу выполнения функции. Это обычная практика оптимизации. Например, если нужно выполнить некоторое вычисление или вызвать другую функцию, компилятор может использовать эти регистры, внезапно "заменяя" их оригинальные значения, но это не означает, что стиль или назначение аргументов изменяются.

  4. LEA инструкции: Слово LEA (Load Effective Address) используется для загрузки адреса в регистр. Это означает, что _Argc и _Argv ссылаются прямо на регистры RCX и RDX, а не на локальные переменные браузерного уровня. Это можно подтвердить, взглянув на соответствующие инструкции и структуру регистров.

  5. Оптимизация: Некоторые оптимизации могут привести к тому, что компилятор использует один и тот же регистр для различных нужд в пределах одной функции. Однако, как только функция возвращает значение, регистры восстанавливаются, и переданные аргументы будут доступны для любых операций, которые должны выполняться с аргументами.

В заключение, хотя название _Argc и _Argv может вводить в заблуждение, это просто замена для более удобного понимания кода. На самом деле, вы все правильно интерпретируете: это не "реподназначение" переменных, а просто использование регистров для временного хранения различных данных согласно правилам оптимизации компилятора.

Оцените материал
Добавить комментарий

Капча загружается...