Понимать ассемблерный код, сгенерированный простой программой на C - PullRequest
15 голосов
/ 07 сентября 2010

Я пытаюсь понять код уровня сборки для простой программы на C, проверяя его с помощью дизассемблера gdb.

Ниже приведен код C:

#include <stdio.h>

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

void main() {
  function(1,2,3);
}

Ниже приведен код разборкии для main, и для function

gdb) disass main
Dump of assembler code for function main:
0x08048428 <main+0>:    push   %ebp
0x08048429 <main+1>:    mov    %esp,%ebp
0x0804842b <main+3>:    and    $0xfffffff0,%esp
0x0804842e <main+6>:    sub    $0x10,%esp
0x08048431 <main+9>:    movl   $0x3,0x8(%esp)
0x08048439 <main+17>:   movl   $0x2,0x4(%esp)
0x08048441 <main+25>:   movl   $0x1,(%esp)
0x08048448 <main+32>:   call   0x8048404 <function>
0x0804844d <main+37>:   leave  
0x0804844e <main+38>:   ret
End of assembler dump.

(gdb) disass function
Dump of assembler code for function function:
0x08048404 <function+0>:    push   %ebp
0x08048405 <function+1>:    mov    %esp,%ebp
0x08048407 <function+3>:    sub    $0x28,%esp
0x0804840a <function+6>:    mov    %gs:0x14,%eax
0x08048410 <function+12>:   mov    %eax,-0xc(%ebp)
0x08048413 <function+15>:   xor    %eax,%eax
0x08048415 <function+17>:   mov    -0xc(%ebp),%eax
0x08048418 <function+20>:   xor    %gs:0x14,%eax
0x0804841f <function+27>:   je     0x8048426 <function+34>
0x08048421 <function+29>:   call   0x8048340 <__stack_chk_fail@plt>
0x08048426 <function+34>:   leave  
0x08048427 <function+35>:   ret    
End of assembler dump.

Я ищу ответы на следующие вопросы:

  1. как работает адресация, я имею в виду (main + 0), (main + 1), (main + 3)
  2. В основном, почему $ 0xfffffff0, используется% esp
  3. В функции, почему% gs: 0x14,% eax,%eax, -0xc (% ebp) используется.
  4. Если кто-то может объяснить, шаг за шагом, это будет оценено.

Ответы [ 4 ]

41 голосов
/ 07 сентября 2010

Причина «странных» адресов, таких как main+0, main+1, main+3, main+6 и т. Д., Заключается в том, что каждая инструкция занимает переменное число байтов.Например:

main+0: push %ebp

является однобайтовой инструкцией, поэтому следующая инструкция находится в main+1.С другой стороны,

main+3: and $0xfffffff0,%esp

является трехбайтовой инструкцией, поэтому следующая инструкция после этого - main+6.

И, поскольку вы спрашиваете в комментариях, почему movlпо-видимому, занимает переменное число байтов, объяснение этому следующее:

Длина инструкции зависит не только от кода операции (например, movl), но и от режимов адресации для операнды (вещи, над которыми работает код операции).Я специально не проверял ваш код, но подозреваю, что инструкция

movl $0x1,(%esp)

, вероятно, короче, потому что здесь нет смещения - она ​​просто использует esp в качестве адреса.Тогда как что-то вроде:

movl $0x2,0x4(%esp)

требует всего, что movl $0x1,(%esp) делает, плюс дополнительный байт для смещения 0x4.

На самом деле, вот отладкасеанс, показывающий, что я имею в виду:

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

c:\pax> debug
-a
0B52:0100 mov word ptr [di],7
0B52:0104 mov word ptr [di+2],8
0B52:0109 mov word ptr [di+0],7
0B52:010E
-u100,10d
0B52:0100 C7050700      MOV     WORD PTR [DI],0007
0B52:0104 C745020800    MOV     WORD PTR [DI+02],0008
0B52:0109 C745000700    MOV     WORD PTR [DI+00],0007
-q
c:\pax> _

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

Вы также можете видеть, что вы можете кодировать первую и третью инструкциюдвумя разными способами, но в основном они делают одно и то же.


Инструкция and $0xfffffff0,%esp - это способ заставить esp находиться на определенной границе.Это используется для обеспечения правильного выравнивания переменных.Многие обращения к памяти на современных процессорах будут более эффективными, если они будут следовать правилам выравнивания (например, 4-байтовое значение должно быть выровнено по 4-байтовой границе).Некоторые современные процессоры могут даже выдавать ошибку, если вы не будете следовать этим правилам.

После этой инструкции вы гарантируете, что esp меньше или равно его предыдущему значению и выровнено по 16-байтовой границе.


Префикс gs: просто означает использование регистра сегмента gs для доступа к памяти, а не по умолчанию.

Инструкция mov %eax,-0xc(%ebp) означает взять содержимое регистра ebp, вычесть 12 (0xc) и затем поместить значение eax в эту ячейку памяти.


Re объяснение кода,Ваша function функция в основном одна большая неоперация.Сгенерированная сборка ограничивается установкой и разбором стекового фрейма, а также некоторой проверкой повреждения фрейма стека, в которой используется вышеупомянутая %gs:14 ячейка памяти.

Загружает значение из этой локации (возможно, что-то вроде 0xdeadbeef) в кадр стека выполняет свою работу, затем проверяет стек, чтобы убедиться, что он не был поврежден.

Его работа в данном случае - ничто.Итак, все, что вы видите, - это функции администрирования функций.

Настройка стека происходит между function+0 и function+12.После этого все, что нужно сделать, это установить код возврата в eax и разорвать кадр стека, включая проверку повреждения.

Аналогично, main состоит из настройки кадра стека, выдавая параметры для function, вызывая function, разрывая стек и завершая работу.

Комментарии были вставлены в код ниже:

0x08048428 <main+0>:    push   %ebp                 ; save previous value.
0x08048429 <main+1>:    mov    %esp,%ebp            ; create new stack frame.
0x0804842b <main+3>:    and    $0xfffffff0,%esp     ; align to boundary.
0x0804842e <main+6>:    sub    $0x10,%esp           ; make space on stack.

0x08048431 <main+9>:    movl   $0x3,0x8(%esp)       ; push values for function.
0x08048439 <main+17>:   movl   $0x2,0x4(%esp)
0x08048441 <main+25>:   movl   $0x1,(%esp)
0x08048448 <main+32>:   call   0x8048404 <function> ; and call it.

0x0804844d <main+37>:   leave                       ; tear down frame.
0x0804844e <main+38>:   ret                         ; and exit.

0x08048404 <func+0>:    push   %ebp                 ; save previous value.
0x08048405 <func+1>:    mov    %esp,%ebp            ; create new stack frame.
0x08048407 <func+3>:    sub    $0x28,%esp           ; make space on stack.
0x0804840a <func+6>:    mov    %gs:0x14,%eax        ; get sentinel value.
0x08048410 <func+12>:   mov    %eax,-0xc(%ebp)      ; put on stack.

0x08048413 <func+15>:   xor    %eax,%eax            ; set return code 0.

0x08048415 <func+17>:   mov    -0xc(%ebp),%eax      ; get sentinel from stack.
0x08048418 <func+20>:   xor    %gs:0x14,%eax        ; compare with actual.
0x0804841f <func+27>:   je     <func+34>            ; jump if okay.
0x08048421 <func+29>:   call   <_stk_chk_fl>        ; otherwise corrupted stack.
0x08048426 <func+34>:   leave                       ; tear down frame.
0x08048427 <func+35>:   ret                         ; and exit.

Я думаю, что причина для %gs:0x14 может быть видно из приведенного выше, но, на всякий случай, я уточню здесь.

Он использует это значение (часовой), чтобы поместить в текущий кадр стека так, чтобы что-то в функции делало что-то глупоенапример, записать 1024 байта в 20-байтовый массив, созданный в стеке, или, в вашем случае:

char buffer1[5];
strcpy (buffer1, "Hello there, my name is Pax.");

, тогда сторож будет перезаписан, и проверка в конце функции обнаружит это, вызвавфункция сбоя, чтобы вы знали, а затем, вероятно, прерывать, чтобы избежать каких-либо других проблем.

Если бы он поместил 0xdeadbeef в стек и это было изменено на что-то другое, то xor с 0xdeadbeef выдаст ненулевое значение, которое обнаруживается в коде с помощью инструкции je.

Соответствующий бит здесь перефразирован:

          mov    %gs:0x14,%eax     ; get sentinel value.
          mov    %eax,-0xc(%ebp)   ; put on stack.

          ;; Weave your function
          ;;   magic here.

          mov    -0xc(%ebp),%eax   ; get sentinel back from stack.
          xor    %gs:0x14,%eax     ; compare with original value.
          je     stack_ok          ; zero/equal means no corruption.
          call   stack_bad         ; otherwise corrupted stack.
stack_ok: leave                    ; tear down frame.
3 голосов
/ 09 сентября 2010

Лучше попробовать флаг -fno-stack-protector с gcc, чтобы отключить канарейку и увидеть ваши результаты.

3 голосов
/ 07 сентября 2010

Пакс дал окончательный ответ. Однако, для полноты, я решил добавить примечание о том, чтобы сам GCC показывал сборку, которую он генерирует.

Опция -S для GCC говорит ему прекратить компиляцию и записать сборку в файл. Обычно он либо передает этот файл ассемблеру, либо для некоторых целей записывает объектный файл непосредственно сам.

Для примера кода в вопросе:

#include <stdio.h>

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

void main() {
  function(1,2,3);
}

команда gcc -S q3654898.c создает файл с именем q3654898.s:

        .file   "q3654898.c"
        .text
.globl _function
        .def    _function;      .scl    2;      .type   32;     .endef
_function:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $40, %esp
        leave
        ret
        .def    ___main;        .scl    2;      .type   32;     .endef
.globl _main
        .def    _main;  .scl    2;      .type   32;     .endef
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        andl    $-16, %esp
        movl    $0, %eax
        addl    $15, %eax
        addl    $15, %eax
        shrl    $4, %eax
        sall    $4, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $3, 8(%esp)
        movl    $2, 4(%esp)
        movl    $1, (%esp)
        call    _function
        leave
        ret

Одна вещь, которая очевидна, заключается в том, что мой GCC (gcc (GCC) 3.4.5 (mingw-vista special r3)) по умолчанию не включает код проверки стека. Я полагаю, что есть опция командной строки или что, если мне удастся подтолкнуть мою установку MinGW к более современному GCC, чем он мог.

Редактировать: Пакс хочет сделать это, вот еще один способ заставить GCC выполнять больше работы.

C:\Documents and Settings\Ross\My Documents\testing>gcc -Wa,-al q3654898.c
q3654898.c: In function `main':
q3654898.c:8: warning: return type of 'main' is not `int'
GAS LISTING C:\DOCUME~1\Ross\LOCALS~1\Temp/ccLg8pWC.s                   page 1


   1                            .file   "q3654898.c"
   2                            .text
   3                    .globl _function
   4                            .def    _function;      .scl    2;      .type
32;     .endef
   5                    _function:
   6 0000 55                    pushl   %ebp
   7 0001 89E5                  movl    %esp, %ebp
   8 0003 83EC28                subl    $40, %esp
   9 0006 C9                    leave
  10 0007 C3                    ret
  11                            .def    ___main;        .scl    2;      .type
32;     .endef
  12                    .globl _main
  13                            .def    _main;  .scl    2;      .type   32;
.endef
  14                    _main:
  15 0008 55                    pushl   %ebp
  16 0009 89E5                  movl    %esp, %ebp
  17 000b 83EC18                subl    $24, %esp
  18 000e 83E4F0                andl    $-16, %esp
  19 0011 B8000000              movl    $0, %eax
  19      00
  20 0016 83C00F                addl    $15, %eax
  21 0019 83C00F                addl    $15, %eax
  22 001c C1E804                shrl    $4, %eax
  23 001f C1E004                sall    $4, %eax
  24 0022 8945FC                movl    %eax, -4(%ebp)
  25 0025 8B45FC                movl    -4(%ebp), %eax
  26 0028 E8000000              call    __alloca
  26      00
  27 002d E8000000              call    ___main
  27      00
  28 0032 C7442408              movl    $3, 8(%esp)
  28      03000000
  29 003a C7442404              movl    $2, 4(%esp)
  29      02000000
  30 0042 C7042401              movl    $1, (%esp)
  30      000000
  31 0049 E8B2FFFF              call    _function
  31      FF
  32 004e C9                    leave
  33 004f C3                    ret

C:\Documents and Settings\Ross\My Documents\testing>

Здесь мы видим выводной список, созданный ассемблером. (Его зовут GAS, потому что это версия Gnu классического * nix ассемблера as. Где-то там есть юмор.)

Каждая строка имеет большинство следующих полей: номер строки, адрес в текущем разделе, байты, сохраненные по этому адресу, и исходный текст из исходного файла сборки. Адреса являются смещениями в эту часть каждого раздела, предоставленного этим модулем. Этот конкретный модуль имеет содержимое только в разделе .text, в котором хранится исполняемый код. Как правило, вы также найдете упоминания разделов с именами .data и .bss. Многие другие имена используются, и некоторые имеют специальные цели. Прочтите руководство для компоновщика, если вы действительно хотите знать.

2 голосов
/ 07 сентября 2010

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

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

/* corrected calling convention of main() */
int main() {
   function(1,2,3);
   return 0;
}

это то, что я получаю без оптимизации (OSX 10.6, gcc 4.2.1 + патчи Apple)

.globl _function
_function:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L4
"L00000000001$pb":
L4:
    popl    %ebx
    leal    L___stack_chk_guard$non_lazy_ptr-"L00000000001$pb"(%ebx), %eax
    movl    (%eax), %eax
    movl    (%eax), %edx
    movl    %edx, -12(%ebp)
    xorl    %edx, %edx
    leal    L___stack_chk_guard$non_lazy_ptr-"L00000000001$pb"(%ebx), %eax
    movl    (%eax), %eax
    movl    -12(%ebp), %edx
    xorl    (%eax), %edx
    je      L3
    call    ___stack_chk_fail
L3:
    addl    $36, %esp
    popl    %ebx
    leave
    ret
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $3, 8(%esp)
    movl    $2, 4(%esp)
    movl    $1, (%esp)
    call    _function
    movl    $0, %eax
    leave
    ret

Вот так, черт! Но посмотрите, что происходит с -O в командной строке ...

    .text
.globl _function
_function:
    pushl   %ebp
    movl    %esp, %ebp
    leave
    ret
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $0, %eax
    leave
    ret

Конечно, вы рискуете сделать ваш код полностью неузнаваемым, особенно на более высоких уровнях оптимизации и при более сложных вещах. Даже здесь мы видим, что призыв к function был отброшен как бессмысленный. Но я нахожу, что отсутствие необходимости читать десятки ненужных разливов в стеке, как правило, более чем стоит немного почесать мою голову над потоком управления.

...