Почему MSVC не выделяет 32-байтовое теневое пространство в сгенерированном ассемблерном коде?

#c #assembly #visual-c #x86-64 #masm

#c #сборка #visual-c #x86-64 #masm

Вопрос:

Я попытался посмотреть, как MSVC выделяет свои 32 байта теневого пространства, но, похоже, он выделяет только 8 байт теневого пространства.

 // Test.c
int main() {int var1 = 1;}
  

Приведенная выше программа приводит к следующему файлу .asm:

 var1$ = 0

main    PROC
; Test.c
    sub rsp, 24                    ; allocates 24 bytes
    mov DWORD PTR var1$[rsp], 1
    xor eax, eax
    add rsp, 24
    ret 0
main    ENDP
  

Он выделяет только 24 байта. Он выделяет столько же, когда я объявляю 4 переменные, и поскольку каждая переменная равна 4 байтам, это должно означать, что 16 байт из 24 байт используются для объявленных переменных, оставляя 8 байт для теневого пространства.
Только при объявлении 5 переменных он выделяет 40 байт теневого пространства. Почему он выделяет только 8 байт теневого пространства?
Я скомпилировал программу с помощью команды CL Test.c /Fa

Комментарии:

1. Я думаю, вы забыли посчитать обратный адрес, передаваемый вызывающим кодом main , что означает, что указатель стека смещен на 8 байт при входе в main .

2. Это все объясняет!

Ответ №1:

Вычитание 24 из RSP здесь не имеет никакого отношения к теневому пространству. Теневое пространство применяется только в том случае, если main требуется вызвать какую-либо другую 64-разрядную функцию, совместимую с Microsoft ABI. Ваша main функция является конечной функцией (она больше ничего не вызывает), поэтому не требуется выделять дополнительное пространство для теневого пространства. Если вы изменили main вызов чего-либо в библиотеке C / C или WinAPI, вы обнаружите, что для теневого пространства будет добавлено дополнительное пространство для выполнения такого вызова.

Учитывая, что ваша функция имеет дело с 32-разрядными значениями (и без массивов) и не вызывает ничего другого, я не вижу причин, по которым ей нужно выравнивать по 16-байтовой границе или добавлять дополнительные отступы, но это то, что она, похоже, делает. Адрес возврата в стеке смещает стек на 8. Вычитание 24 выравнивает его по 16-байтовой границе с дополнением после переменных.

Вероятно, это результат неэффективности генерации кода при компиляции без каких-либо оптимизаций (например /O1 , /O2 и т.д.) Или компилятора, заполняющего пространство локальных переменных до предпочтительного размера. Теоретически в этом случае не нужно было выделять какое-либо пространство стека. Он мог бы повторно использовать теневое пространство над обратным адресом, которое было бы выделено для main функции кодом запуска C / C .

Примечание: При оптимизации код будет полностью удален, если вы не создадите var1 volatile переменную. Компилятор должен распознать, что написанный вами код ничего не делает, кроме возврата обратно вызывающему.


В следующем примере добавлены вызовы ExitProcess для отображения теневого пространства; повторное использование теневого пространства, выделенного кодом запуска C / C , который вызывал main локальные переменные; и использование некоторого стекового пространства для переменной, которая не может поместиться в теневом пространстве. Поскольку вызываемый WinAPI ExitProcess должен иметь 32 байта теневого пространства, выделенного перед вызовом к нему. Если вы удалите его из этого примера, компилятор не выделит для него дополнительное пространство.

test.c

 // Test.c

// Get prototype for ExitProcess
#include <windows.h>

int main() 
{
    volatile int var1 = 1;
    volatile int var2 = 2;
    volatile int var3 = 3;
    volatile int var4 = 4;
    volatile int var5 = 5;

    // Since this is a WinAPI call it needs shadow space allocated
    ExitProcess(var1 var2 var3 var4 var5);

    // We won't get this far
    return 0;
}
  

Если вы скомпилируете это с /O2 оптимизацией для максимальной скорости, используя CL Test.c /Fa /O2 , вы, вероятно, увидите что-то похожее на:

 var1$ = 32
var5$ = 64
var4$ = 72
var3$ = 80
var2$ = 88

main    PROC
    sub rsp, 56                 ; 00000038H
    mov DWORD PTR var1$[rsp], 1
    mov DWORD PTR var2$[rsp], 2
    mov DWORD PTR var3$[rsp], 3
    mov DWORD PTR var4$[rsp], 4
    mov DWORD PTR var5$[rsp], 5

    mov edx, DWORD PTR var5$[rsp]
    mov eax, DWORD PTR var4$[rsp]
    add edx, eax
    mov ecx, DWORD PTR var3$[rsp]
    add ecx, edx
    mov edx, DWORD PTR var2$[rsp]
    add edx, ecx
    mov ecx, DWORD PTR var1$[rsp]
    add ecx, edx

    call    QWORD PTR __imp_ExitProcess
    int 3
main    ENDP
  

var1 имеет смещение 32 от RSP, потому что теневое пространство — это первые 32 байта, начинающиеся с RSP для вызова ExitProcess . Другие переменные var2 , var3 , var4 и var5 все начинаются со смещения> = 64. Компилятор сгенерировал корректировку 56 для RSP. Адрес возврата находится в RSP 56 и main имеет теневое пространство от RSP 64 до RSP 96, таким образом var2 , to var5 были помещены в теневое пространство, выделенное для main .