Обратный инжиниринг C-исходного кода из сборки - PullRequest
5 голосов
/ 13 ноября 2011

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

C Источник данных:

int arith(int x, int y, int z)
{ 
   int t1 = x+y;
   int t2 = z+t1;
   int t3 = x+4;
   int t4 = y * 48; 
   int t5 = t3 + t4;
   int rval = t2 * t5;
   return rval;
}

Сборка дана:

arith:
pushl %ebp
movl %esp,%ebp

movl 8(%ebp),%eax
movl 12(%ebp),%edx
leal (%edx,%eax),%ecx
leal (%edx,%edx,2),%edx
sall $4,%edx
addl 16(%ebp),%ecx
leal 4(%edx,%eax),%eax
imull %ecx,%eax

movl %ebp,%esp
popl %ebp
ret

Меня просто смущает, как я могу различить, например, что добавление z + t1 (z + x + y) указано во второй строке (в источнике), когда в сборке оно идет послеy * 48 в коде сборки или, например, x + 4 - это третья строка, когда в сборке она даже не находится в отдельной строке, это своего рода смешивается с последним оператором leal.Это имеет смысл для меня, когда у меня есть источник, но я должен быть в состоянии воспроизвести источник для теста, и я понимаю, что компилятор оптимизирует вещи, но если у кого-то есть способ думать об обратном инжиниринге, который мог бы помочь мнеЯ был бы очень признателен, если бы они могли провести меня через их мыслительный процесс.

Спасибо.

Ответы [ 4 ]

9 голосов
/ 14 ноября 2011

Я разбил разборку для вас, чтобы показать, как сборка была произведена из источника C.

8(%ebp) = x, 12(%ebp) = y, 16(%ebp) = z

arith:

Создать кадр стека:

pushl %ebp
movl %esp,%ebp

<ч /> Переместить x в eax, y в edx:

movl 8(%ebp),%eax
movl 12(%ebp),%edx

<ч /> t1 = x + y. leal (Загрузить эффективный адрес) добавит edx и eax, а t1 будет в ecx:

leal (%edx,%eax),%ecx

<ч /> int t4 = y * 48; в два этапа ниже, умножьте на 3, затем на 16. t4 в конечном итоге будет в edx:

Умножьте edx на 2 и добавьте edx к результату, т.е. edx = edx * 3

leal (%edx,%edx,2),%edx

Сдвиг влево на 4 бита, т.е. умножить на 16:

sall $4,%edx

<ч /> int t2 = z+t1;. ecx первоначально содержит t1, z находится в 16(%ebp), в конце инструкции ecx будет удерживать t2:

addl 16(%ebp),%ecx

<ч /> int t5 = t3 + t4;. t3 было просто x + 4, и вместо вычисления и хранения t3 выражение t3 помещается в строку. Эта инструкция обязательна для (x+4) + t4, что аналогично t3 + t4. Он добавляет edx (t4) и eax (x) и добавляет 4 как смещение для достижения этого результата.

leal 4(%edx,%eax),%eax
<Ч />

int rval = t2 * t5; Довольно прямолинейно; ecx представляет t2, а eax представляет t5. Возвращаемое значение передается вызывающей стороне через eax.

imull %ecx,%eax

<ч /> Уничтожить кадр стека и восстановить esp и ebp:

movl %ebp,%esp
popl %ebp

<ч /> Возвращение из рутины:

ret

<ч /> Из этого примера вы можете видеть, что результат тот же, но структура немного другая. Скорее всего, этот код был скомпилирован с какой-то оптимизацией, или кто-то написал его сам, чтобы продемонстрировать свою точку зрения.

Как уже говорили другие, вы не можете точно вернуться к источнику после разборки. Человек, читающий сборку, должен интерпретировать эквивалентный код С.

<ч /> Чтобы помочь в изучении ассемблера и понимания дизассемблирования ваших программ на C, вы можете сделать следующее в Linux:

Компиляция с отладочной информацией (-g), которая будет включать источник:

gcc -c -g arith.c

Если вы работаете на 64-битной машине, вы можете указать компилятору создать 32-битный бинарный файл с флагом -m32 (я сделал это для примера ниже).


Используйте objdump для выгрузки объектного файла с чередованием его источника:

objdump -d -S arith.o

-d = разборка, -S = источник отображения. Вы можете добавить -M intel-mnemonic, чтобы использовать синтаксис Intel ASM, если вы предпочитаете его синтаксису AT & T, который используется в вашем примере.

Выход:

arith.o:     file format elf32-i386


Disassembly of section .text:

00000000 <arith>:
int arith(int x, int y, int z)
{ 
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 20                sub    $0x20,%esp
   int t1 = x+y;
   6:   8b 45 0c                mov    0xc(%ebp),%eax
   9:   8b 55 08                mov    0x8(%ebp),%edx
   c:   01 d0                   add    %edx,%eax
   e:   89 45 fc                mov    %eax,-0x4(%ebp)
   int t2 = z+t1;
  11:   8b 45 fc                mov    -0x4(%ebp),%eax
  14:   8b 55 10                mov    0x10(%ebp),%edx
  17:   01 d0                   add    %edx,%eax
  19:   89 45 f8                mov    %eax,-0x8(%ebp)
   int t3 = x+4;
  1c:   8b 45 08                mov    0x8(%ebp),%eax
  1f:   83 c0 04                add    $0x4,%eax
  22:   89 45 f4                mov    %eax,-0xc(%ebp)
   int t4 = y * 48; 
  25:   8b 55 0c                mov    0xc(%ebp),%edx
  28:   89 d0                   mov    %edx,%eax
  2a:   01 c0                   add    %eax,%eax
  2c:   01 d0                   add    %edx,%eax
  2e:   c1 e0 04                shl    $0x4,%eax
  31:   89 45 f0                mov    %eax,-0x10(%ebp)
   int t5 = t3 + t4;
  34:   8b 45 f0                mov    -0x10(%ebp),%eax
  37:   8b 55 f4                mov    -0xc(%ebp),%edx
  3a:   01 d0                   add    %edx,%eax
  3c:   89 45 ec                mov    %eax,-0x14(%ebp)
   int rval = t2 * t5;
  3f:   8b 45 f8                mov    -0x8(%ebp),%eax
  42:   0f af 45 ec             imul   -0x14(%ebp),%eax
  46:   89 45 e8                mov    %eax,-0x18(%ebp)
   return rval;
  49:   8b 45 e8                mov    -0x18(%ebp),%eax
}
  4c:   c9                      leave  
  4d:   c3                      ret

Как видите, без оптимизации компилятор создает бинарный файл большего размера, чем в вашем примере. Вы можете поэкспериментировать с этим и добавить флаг оптимизации компилятора при компиляции (т. Е. -O1, -O2, -O3). Чем выше уровень оптимизации, тем более абстрактной будет казаться разборка.

Например, при оптимизации уровня 1 (gcc -c -g -O1 -m32 arith.c1) полученный код сборки значительно короче:

00000000 <arith>:
int arith(int x, int y, int z)
{ 
   0:   8b 4c 24 04             mov    0x4(%esp),%ecx
   4:   8b 54 24 08             mov    0x8(%esp),%edx
   int t1 = x+y;
   8:   8d 04 11                lea    (%ecx,%edx,1),%eax
   int t2 = z+t1;
   b:   03 44 24 0c             add    0xc(%esp),%eax
   int t3 = x+4;
   int t4 = y * 48; 
   f:   8d 14 52                lea    (%edx,%edx,2),%edx
  12:   c1 e2 04                shl    $0x4,%edx
   int t5 = t3 + t4;
  15:   8d 54 11 04             lea    0x4(%ecx,%edx,1),%edx
   int rval = t2 * t5;
  19:   0f af c2                imul   %edx,%eax
   return rval;
}
  1c:   c3                      ret
6 голосов
/ 13 ноября 2011

Вы не можете воспроизвести исходный источник, вы можете воспроизвести только эквивалентный источник.

В вашем случае расчет для t2 может появиться где угодно после t1 и до retval.

Источником могло быть даже одно выражение:

return (x+y+z) * ((x+4) + (y * 48));
5 голосов
/ 13 ноября 2011

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

1 голос
/ 13 ноября 2011

Декомпиляция не вполне достижима: происходит некоторая потеря знаний при переходе от исходного кода (где комментарии и имена дали вам подсказку о намерениях исходного программиста) к двоичному машинному коду (где инструкциивыполняется процессором).

...