Почему, когда я обращаюсь к объекту, состоящему из трех целых чисел, он вычитает из базового указателя, а не из указателя стека?

Я хотел выяснить, как работают объекты, посмотрев на ассемблерный вывод программы. У меня есть класс Numbers, содержащий три класса ints.

class Numbers {

public:

    int n1;
    int n2;
    int n3;

};

Внутри основной функции я создаю экземпляр с именем obj и присваиваю каждой из переменных число.

int main() {

    Numbers obj;

    obj.n1 = 1;
    obj.n2 = 2;
    obj.n3 = 3;

}

Следующий код представляет собой сгенерированную сборку:

int main() {
00935240  push        ebp  
00935241  mov         ebp,esp  
00935243  sub         esp,0D8h  
00935249  push        ebx  
0093524A  push        esi  
0093524B  push        edi  
0093524C  lea         edi,[ebp-0D8h]  
00935252  mov         ecx,36h  
00935257  mov         eax,0CCCCCCCCh  
0093525C  rep stos    dword ptr es:[edi]  
0093525E  mov         eax,dword ptr ds:[0093F000h]  
00935263  xor         eax,ebp  
00935265  mov         dword ptr [ebp-4],eax  

    Numbers obj;

    obj.n1 = 1;
00935268  mov         dword ptr [obj],1       ; === Here ===
    obj.n2 = 2;
0093526F  mov         dword ptr [ebp-10h],2   ; === Here ===
    obj.n3 = 3;
00935276  mov         dword ptr [ebp-0Ch],3   ; === Here ===

    return 0;
0093527D  xor         eax,eax  
}

Я думал, что базовый указатель указывает на вершину стекового фрейма, а так как функция основная, то указывает на начало программы. Как он может вычитать из базового указателя, когда указатель стека указывает на текущий адрес? Кроме того, почему он обращается к переменным не по порядку. Он изменяет n1, затем вычитает 16 байтов, чтобы получить адрес n2, а затем 12 байтов, чтобы добраться до n3. Есть ли причина для этого?

Я использую Visual Studio 2013 с MASM в качестве ассемблера.


person Greg M    schedule 10.02.2016    source источник
comment
локальные переменные ниже ebp, адрес возврата и аргументы функции выше ebp. Вы можете ссылаться на данные стека из ebp или esp, если вы отслеживаете, как был изменен esp с тех пор, как вы создали кадр стека. Или, для оптимизированного кода, даже не создавайте кадры стека. (по умолчанию во многих компиляторах).   -  person Peter Cordes    schedule 10.02.2016
comment
Стек растет вниз, как вы можете видеть из sub esp,0D8h. Поскольку до этого он был сохранен в ebp, все выделенное пространство находится с отрицательным смещением от ebp. dword ptr [obj] вводит в заблуждение, я почти уверен, что это тоже ebp-относительная адресация. Используя правильный дизассемблер, вы должны увидеть там ebp-14h, поэтому n1, n2 и n3 расположены в ожидаемом порядке возрастания.   -  person Jester    schedule 10.02.2016
comment
Локальные переменные всегда размещаются в стеке, если только они не помечены как статические.   -  person David Hoelzer    schedule 10.02.2016
comment
@DavidHoelzer: компилятор может использовать любой подходящий механизм. Некоторые локальные переменные могут появляться в регистрах в качестве примера.   -  person Michael Petch    schedule 10.02.2016


Ответы (2)


Между ожиданием медленного запуска Apache Pluto и ожиданием того, что мой босс закончит свой звонок, мне нравится бродить по этой пустыне вопросов, помеченных сборка.
Так что именно в этом скучающем настроении я пишу еще один бесполезный ответ на этот уже удовлетворенный ОП.

;PROLOG

push ebp                    ;Save the caller frame pointer
mov ebp, esp                ;Make our frame pointer 

;ALLOCATE SPACE

sub esp, 0D8h               ;Reserve 216 bytes on the stack 
                            ;Why 216? I dunno, maybe this makes the compiler
                            ;source code easy to write/read/mantain

;SAVE CALLER REGS

push ebx 
push esi 
push edi                    ;Save caller register that we must not clobber

;INIT ALLOCATED SPACE

lea edi, [ebp-0D8h]         ;EDI point to the start (the lower limit) of 
                            ;our reserved space (EDI = EBP-0d8h)

mov ecx, 36h                ;ECX is the number of DWORD to write, 
                            ;36h*4 = 0d8h = 216 bytes

mov eax, 0CCCCCCCCh         ;EAX is the DWORD to write, 0cccccccch comes 
                            ;from the fact that: 1) 0cch is the opcode for 
                            ;int 03h which is by convention the debug exception 
                            ;2) it is easy to spot 3) it is an invalid address to
                            ;deference. This way an uninitialized var will misbehave 
                            ;when used (not for arithmetic). This is for debug purpose.

rep stos dword ptr es:[edi] ;Write ECX times EAX from ES:EDI upward (N.B. UPWARD) 

;SET UP THE CANARY

mov eax, dword ptr ds:[0093F000h]   ;Take a value which is safe in memory and cannot be
                                    ;overwritten by stack overflow (those guys, grrrr...)

xor eax, ebp                        ;Compute a function of the frame pointer and the canary
                                    ;This can make the canary unique on every invocation.
                                    ;The function is a xor 

mov dword ptr [ebp-4],eax           ;The canary is at the very beginning (ending?) or our 
                                    ;allocated space. It is just below the frame pointer.


;Set the object fields                                  

mov dword ptr [obj], 1              ;I believe this obj is [ebp-14h]
mov dword ptr [ebp-10h], 2          ;Remember that [ebp-10h] is after [ebp-14h], just like
mov dword ptr [ebp-0Ch], 3          ;-10 is after (i.e. bigger than)  -14. 


;Return the value 0

xor eax,eax                         ;EAX have to hold the returned value at the end of the 
                                    ;function, V XOR V = 0 for all V 
person Margaret Bloom    schedule 10.02.2016
comment
Я не нашел это бесполезным, я действительно думал, что это было весьма полезно. Спасибо, что написали это. - person Greg M; 10.02.2016

Регистр ebp обычно указывает на начало начала кадра стека текущей функции (которая обычно содержит указатель на предыдущий кадр) в стеке.

В выходных данных сборки сначала сохраняется последний указатель кадра стека, затем адрес текущего указателя стека сохраняется в ebp. Это начало кадра стека для текущей функции. Затем из esp вычитается несколько байтов, чтобы зарезервировать место в стеке для локальных переменных.

Порядок переменных правильный; адреса переменных находятся ниже текущего адреса ebp (стек растет от старших адресов к нижним).

Я думал, что базовый указатель указывает на вершину стекового фрейма, а так как функция основная, то на старт программы.

main() на самом деле не первая вызываемая функция; есть много функций запуска libc, вызываемых ранее (например, для глобальной инициализации объекта).

person ul90    schedule 10.02.2016