Может ли кто-нибудь помочь мне интерпретировать эту простую разборку из WinDbg? - PullRequest
11 голосов
/ 26 октября 2010

Я получил следующий простой код C ++:

#include <stdio.h>
int main(void)
{
    ::printf("\nHello,debugger!\n");
}

И из WinDbg я получил следующий код разборки:

SimpleDemo!main:
01111380 55              push    ebp
01111381 8bec            mov     ebp,esp
01111383 81ecc0000000    sub     esp,0C0h
01111389 53              push    ebx
0111138a 56              push    esi
0111138b 57              push    edi
0111138c 8dbd40ffffff    lea     edi,[ebp-0C0h]
01111392 b930000000      mov     ecx,30h
01111397 b8cccccccc      mov     eax,0CCCCCCCCh
0111139c f3ab            rep stos dword ptr es:[edi]
0111139e 8bf4            mov     esi,esp
011113a0 683c571101      push    offset SimpleDemo!`string' (0111573c)
011113a5 ff15b0821101    call    dword ptr [SimpleDemo!_imp__printf (011182b0)]
011113ab 83c404          add     esp,4
011113ae 3bf4            cmp     esi,esp
011113b0 e877fdffff      call    SimpleDemo!ILT+295(__RTC_CheckEsp) (0111112c)
011113b5 33c0            xor     eax,eax
011113b7 5f              pop     edi
011113b8 5e              pop     esi
011113b9 5b              pop     ebx
011113ba 81c4c0000000    add     esp,0C0h
011113c0 3bec            cmp     ebp,esp
011113c2 e865fdffff      call    SimpleDemo!ILT+295(__RTC_CheckEsp) (0111112c)
011113c7 8be5            mov     esp,ebp
011113c9 5d              pop     ebp
011113ca c3              ret

У меня есть некоторые трудности, чтобы полностью понять это. Что здесь делают SimpleDemo! ILT ?

Какой смысл инструкции сравнивать ebp и esp в 011113c0 ?

Поскольку у меня нет никаких локальных переменных в функции main (), почему по-прежнему есть sub esp, 0C0h при расположении 01111383

Большое спасибо.

Обновление 1

Хотя я до сих пор не знаю, что означает ILT , но __RTC_CheckESP предназначен для проверок во время выполнения. Этот код можно убрать, поместив следующую прагму перед функцией main () .

#pragma runtime_checks( "su", off )

Ссылка:

http://msdn.microsoft.com/en-us/library/8wtf2dfz.aspx

http://msdn.microsoft.com/en-us/library/6kasb93x.aspx

Обновление 2

Инструкция sub esp, 0C0h выделяет еще один 0C0h байт дополнительного пространства в стеке. Затем EAX заполняется 0xCCCCCCCC, это 4 байта, поскольку ECX = 30h, 4 * 30h = 0C0h, поэтому инструкция rep Stos dword ptr es: [edi] заполняет точно дополнительные пробелы с 0xCC. Но для чего это дополнительное пространство в стеке? Это какой-то безопасный ремень? Также я замечаю, что если я отключу проверку во время выполнения, как показывает Обновление 1, в стеке все еще остается такое дополнительное пространство, хотя и намного меньше. И это пространство не заполнено 0xCC.

Код сборки без проверки времени выполнения приведен ниже:

SimpleDemo!main:
00231250 55              push    ebp
00231251 8bec            mov     ebp,esp
00231253 83ec40          sub     esp,40h <-- Still extra space allocated from stack, but smaller
00231256 53              push    ebx
00231257 56              push    esi
00231258 57              push    edi
00231259 683c472300      push    offset SimpleDemo!`string' (0023473c)
0023125e ff1538722300    call    dword ptr [SimpleDemo!_imp__printf (00237238)]
00231264 83c404          add     esp,4
00231267 33c0            xor     eax,eax
00231269 5f              pop     edi
0023126a 5e              pop     esi
0023126b 5b              pop     ebx
0023126c 8be5            mov     esp,ebp
0023126e 5d              pop     ebp
0023126f c3              ret

Ответы [ 5 ]

33 голосов
/ 26 октября 2010

Я комментировал ассемблер, надеюсь, это вам немного поможет.Строки, начинающиеся с «d», являются строками кода отладки, строки, начинающиеся с «r», являются строками кода проверки во время выполнения.Я также добавил то, что, по моему мнению, будет выглядеть как отладочная версия без проверки времени выполнения и версии выпуска.

  ; The ebp register is used to access local variables that are stored on the stack, 
  ; this is known as a stack frame. Before we start doing anything, we need to save 
  ; the stack frame of the calling function so it can be restored when we finish.
  push    ebp                   
  ; These two instructions create our stack frame, in this case, 192 bytes
  ; This space, although not used in this case, is useful for edit-and-continue. If you
  ; break the program and add code which requires a local variable, the space is 
  ; available for it. This is much simpler than trying to relocate stack variables, 
  ; especially if you have pointers to stack variables.
  mov     ebp,esp             
d sub     esp,0C0h              
  ; C/C++ functions shouldn't alter these three registers in this build configuration,
  ; so save them. These are stored below our stack frame (the stack moves down in memory)
r push    ebx
r push    esi
r push    edi                   
  ; This puts the address of the stack frame bottom (lowest address) into edi...
d lea     edi,[ebp-0C0h]        
  ; ...and then fill the stack frame with the uninitialised data value (ecx = number of
  ; dwords, eax = value to store)
d mov     ecx,30h
d mov     eax,0CCCCCCCCh     
d rep stos dword ptr es:[edi]   
  ; Stack checking code: the stack pointer is stored in esi
r mov     esi,esp               
  ; This is the first parameter to printf. Parameters are pushed onto the stack 
  ; in reverse order (i.e. last parameter pushed first) before calling the function.
  push    offset SimpleDemo!`string' 
  ; This is the call to printf. Note the call is indirect, the target address is
  ; specified in the memory address SimpleDemo!_imp__printf, which is filled in when
  ; the executable is loaded into RAM.
  call    dword ptr [SimpleDemo!_imp__printf] 
  ; In C/C++, the caller is responsible for removing the parameters. This is because
  ; the caller is the only code that knows how many parameters were put on the stack
  ; (thanks to the '...' parameter type)
  add     esp,4                 
  ; More stack checking code - this sets the zero flag if the stack pointer is pointing
  ; where we expect it to be pointing. 
r cmp     esi,esp               
  ; ILT - Import Lookup Table? This is a statically linked function which throws an
  ; exception/error if the zero flag is cleared (i.e. the stack pointer is pointing
  ; somewhere unexpected)
r call    SimpleDemo!ILT+295(__RTC_CheckEsp)) 
  ; The return value is stored in eax by convention
  xor     eax,eax               
  ; Restore the values we shouldn't have altered
r pop     edi
r pop     esi
r pop     ebx                   
  ; Destroy the stack frame
r add     esp,0C0h              
  ; More stack checking code - this sets the zero flag if the stack pointer is pointing
  ; where we expect it to be pointing. 
r cmp     ebp,esp               
  ; see above
r call    SimpleDemo!ILT+295(__RTC_CheckEsp) 
  ; This is the usual way to destroy the stack frame, but here it's not really necessary
  ; since ebp==esp
  mov     esp,ebp               
  ; Restore the caller's stack frame
  pop     ebp                   
  ; And exit
  ret                           


  ; Debug only, no runtime checks  
  push    ebp                   
  mov     ebp,esp             
d sub     esp,0C0h              
d lea     edi,[ebp-0C0h]        
d mov     ecx,30h
d mov     eax,0CCCCCCCCh     
d rep stos dword ptr es:[edi]   
  push    offset SimpleDemo!`string' 
  call    dword ptr [SimpleDemo!_imp__printf] 
  add     esp,4                 
  xor     eax,eax               
  mov     esp,ebp               
  pop     ebp                   
  ret                             


  ; Release mode (I'm assuming the optimiser is clever enough to drop the stack frame when there's no local variables)
  push    offset SimpleDemo!`string' 
  call    dword ptr [SimpleDemo!_imp__printf] 
  add     esp,4                 
  xor     eax,eax               
  ret                               
2 голосов
/ 26 октября 2010

Номер один main () вашего кода сформирован неправильно. Он не возвращает int, который вы обещали вернуть. Исправляя этот дефект, мы получаем:

#include 
int main(int argc, char *argv[])
{
    ::printf("\nHello,debugger!\n");
    return 0;
}

Кроме того, более странно видеть #include <stdio.h> в программе на C ++. Я верю, что вы хотите #include <cstdio>

Во всех случаях в стеке должно быть место для аргументов и возвращаемых значений. Возвращаемое значение main () требует места в стеке. Контекст main () для сохранения во время вызова функции printf () требует стекового пространства. Аргументы printf () требуют места в стеке. Возвращаемое значение printf () требует места в стеке. Вот что делает фрейм стека байтов 0c0h.

Первое, что происходит, это то, что входящий указатель bas копируется в верхнюю часть стека. Затем новый указатель стека копируется в базовый указатель. Позже мы проверим, чтобы убедиться, что стек возвращается туда, откуда он начался (потому что у вас включена проверка во время выполнения). Затем мы создаем фрейм стека (длиной 0C0h байт) для хранения нашего контекста и аргументов printf () во время вызова printf (). Переходим к printf (). Когда мы вернемся, мы перепрыгнем через возвращаемое значение, которое вы не проверили в своем коде (единственное, что осталось в его кадре) и убедимся, что стек после вызова находится в том же месте, в котором был до вызова. Мы выталкиваем наш контекст обратно из стека. Затем мы проверяем, что окончательный указатель стека соответствует значению, которое мы сохранили в начале. Затем мы выталкиваем предыдущее значение базового указателя с самого верха стека и возвращаем.

0 голосов
/ 05 марта 2013

Для записи, я подозреваю, что ILT означает «Инкрементное связывание Thunk».

Способ инкрементного связывания (и Edit & Continue) работает следующим образом: компоновщик добавляет слой косвенности для каждого вызова через thunks, которыйсгруппированы в начале исполняемого файла и после них добавляет огромное зарезервированное пространство.Таким образом, когда вы перезагружаете обновленный исполняемый файл, он может просто поместить любой новый / измененный код в зарезервированную область и исправить только исправленные блоки, не изменяя остальную часть кода.

0 голосов
/ 27 октября 2010

40 байтов - это выделение стека в наихудшем случае для любой вызываемой или впоследствии вызываемой функции.Это объясняется в мельчайших подробностях здесь .

Для чего это место зарезервировано на вершине стека?Во-первых, пространство создается для любых локальных переменных.В этом случае FunctionWith6Params () имеет два.Однако эти две локальные переменные составляют только 0x10 байтов.Как обстоят дела с остальным пространством, созданным в верхней части стека?
На платформе x64, когда код готовит стек для вызова другой функции, он не использует инструкции push для помещения параметров встек, как это обычно бывает в коде x86.Вместо этого указатель стека обычно остается фиксированным для конкретной функции.Компилятор просматривает все функции, которые вызывает код в текущей функции, находит ту с максимальным количеством параметров, а затем создает в стеке достаточно места для размещения этих параметров.В этом примере FunctionWith6Params () вызывает функцию printf (), передавая ей 8 параметров.Поскольку это вызываемая функция с максимальным количеством параметров, компилятор создает 8 слотов в стеке.Четыре верхних слота в стеке станут домашним пространством, используемым любыми функциями FunctionWith6Params ().
0 голосов
/ 26 октября 2010

Это код, который вставляется компилятором при сборке с проверкой во время выполнения (/ RTC) .Отключите эти параметры, и это должно быть понятнее. / GZ также может быть причиной этого в зависимости от версии VS.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...