Различия в дизассемблированном C-коде GCC и Borland? - PullRequest
2 голосов
/ 04 декабря 2010

Недавно я заинтересовался дизассемблированием C-кода (очень простого C-кода) и следовал руководству, в котором использовался Borland C ++ Compiler v 5.5 (компилирует C-код очень хорошо), и все работало.Затем я решил попробовать свой собственный c-код и скомпилировал их в Dev C ++ (который использует gcc).Открыв его в IDA Pro, я получил сюрприз: ассемблер gcc был действительно другим по сравнению с Borland.Я ожидал некоторой разницы, но код на C был ЧРЕЗВЫЧАЙНО прост, так что просто gcc не оптимизирует так много или они используют разные настройки компилятора по умолчанию?

Код C

int main(int argc, char **argv)
{
   int a;
   a = 1;
}

Borland ASM

.text:00401150 ; int __cdecl main(int argc,const char **argv,const char *envp)
.text:00401150 _main           proc near               ; DATA XREF: .data:004090D0
.text:00401150
.text:00401150 argc            = dword ptr  8
.text:00401150 argv            = dword ptr  0Ch
.text:00401150 envp            = dword ptr  10h
.text:00401150
.text:00401150                 push    ebp
.text:00401151                 mov     ebp, esp
.text:00401153                 pop     ebp
.text:00401154                 retn
.text:00401154 _main           endp

GCC ASM (ОБНОВЛЕНО ЖЕЛТЫЙ)

.text:00401220 ; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦ S U B R O U T I N E ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
.text:00401220
.text:00401220 ; Attributes: bp-based frame
.text:00401220
.text:00401220                 public start
.text:00401220 start           proc near
.text:00401220
.text:00401220 var_14          = dword ptr -14h
.text:00401220 var_8           = dword ptr -8
.text:00401220
.text:00401220                 push    ebp
.text:00401221                 mov     ebp, esp
.text:00401223                 sub     esp, 8
.text:00401226                 mov     [esp+8+var_8], 1
.text:0040122D                 call    ds:__set_app_type
.text:00401233                 call    sub_401100
.text:00401238                 nop
.text:00401239                 lea     esi, [esi+0]
.text:00401240                 push    ebp
.text:00401241                 mov     ebp, esp
.text:00401243                 sub     esp, 8
.text:00401246                 mov     [esp+14h+var_14], 2
.text:0040124D                 call    ds:__set_app_type
.text:00401253                 call    sub_401100
.text:00401258                 nop
.text:00401259                 lea     esi, [esi+0]
.text:00401259 start           endp

Обновление GCC При следовании предложению JimR я пошел посмотреть, что такое sub_401100, а затем я следовал этому кодук другому, и это, кажется, код (я прав в этом предположении, и если sowhy имеет ли GCC весь свой код в основной функции?):

.text:00401100 sub_401100      proc near               ; CODE XREF: .text:004010F1j
.text:00401100                                         ; start+13p ...
.text:00401100
.text:00401100 var_28          = dword ptr -28h
.text:00401100 var_24          = dword ptr -24h
.text:00401100 var_20          = dword ptr -20h
.text:00401100 var_1C          = dword ptr -1Ch
.text:00401100 var_18          = dword ptr -18h
.text:00401100 var_C           = dword ptr -0Ch
.text:00401100 var_8           = dword ptr -8
.text:00401100
.text:00401100                 push    ebp
.text:00401101                 mov     ebp, esp
.text:00401103                 push    ebx
.text:00401104                 sub     esp, 24h        ; lpTopLevelExceptionFilter
.text:00401107                 lea     ebx, [ebp+var_8]
.text:0040110A                 mov     [esp+28h+var_28], offset sub_401000
.text:00401111                 call    SetUnhandledExceptionFilter
.text:00401116                 sub     esp, 4          ; uExitCode
.text:00401119                 call    sub_4012E0
.text:0040111E                 mov     [ebp+var_8], 0
.text:00401125                 mov     eax, offset dword_404000
.text:0040112A                 lea     edx, [ebp+var_C]
.text:0040112D                 mov     [esp+28h+var_18], ebx
.text:00401131                 mov     ecx, dword_402000
.text:00401137                 mov     [esp+28h+var_24], eax
.text:0040113B                 mov     [esp+28h+var_20], edx
.text:0040113F                 mov     [esp+28h+var_1C], ecx
.text:00401143                 mov     [esp+28h+var_28], offset dword_404004
.text:0040114A                 call    __getmainargs
.text:0040114F                 mov     eax, ds:dword_404010
.text:00401154                 test    eax, eax
.text:00401156                 jz      short loc_4011B0
.text:00401158                 mov     dword_402010, eax
.text:0040115D                 mov     edx, ds:_iob
.text:00401163                 test    edx, edx
.text:00401165                 jnz     loc_4011F6

.text:004012E0 sub_4012E0      proc near               ; CODE XREF: sub_401000+C6p
.text:004012E0                                         ; sub_401100+19p
.text:004012E0                 push    ebp
.text:004012E1                 mov     ebp, esp
.text:004012E3                 fninit
.text:004012E5                 pop     ebp
.text:004012E6                 retn
.text:004012E6 sub_4012E0      endp

Ответы [ 6 ]

3 голосов
/ 05 декабря 2010

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

Точно так же один и тот же компилятор с разными параметрами компилятора может и часто будет производить совершенно разные выходные данные для одного и того же исходного кода. Даже для простых программ.

В случае вашей простой программы, которая на самом деле ничего не делает (код не влияет ни на ввод, ни на вывод, ни на что-либо вне функции), хороший оптимизированный компилятор не даст ничего, кроме main: с возвратом случайное число, так как вы не указали возвращаемое значение. На самом деле это должно дать предупреждение или ошибку. Это самая большая проблема, с которой я сталкиваюсь, когда сравниваю вывод компилятора, делая что-то достаточно простое, чтобы увидеть, что они делают, но что-то достаточно сложное, чтобы компилятор делал больше, чем просто предварительно вычисляет ответ и возвращает его.

В случае с x86, о котором вы, я полагаю, говорите, поскольку в наши дни микрокодирование действительно не дает ответа на вопрос «хороший код против плохого кода», в каждом семействе процессоров они меняют внутренности и то, что раньше было Быстро работает медленно, а то, что сейчас быстро, медленно на старом процессоре. Таким образом, для компиляторов, таких как gcc, которые продолжают развиваться с новыми ядрами, оптимизация может быть как общей для всех x86, так и специфичной для конкретного семейства (что приводит к другому коду, несмотря на максимальную оптимизацию).

С новым интересом к дизассемблированию вы продолжите видеть сходства и различия и узнаете, сколько разных способов можно скомпилировать одним и тем же кодом. Различия ожидаются даже для тривиальных программ. И я призываю вас попробовать как можно больше компиляторов. Даже в семействе gcc 2.x, 3.x, 4.x и различных способах его создания будет разный код, который можно рассматривать как один и тот же компилятор.

Хороший или плохой выход - в глазах смотрящего. Люди, которые используют отладчики, захотят, чтобы их код можно было переставлять, а их переменные можно было наблюдать (в порядке написания кода). Это делает очень большой, громоздкий и медленный код (особенно для x86). А когда вы компилируете релиз, вы получаете совершенно другую программу, которую вы до сих пор тратили на отладку без затрат времени. Кроме того, оптимизируя производительность, вы рискуете компилятором оптимизировать то, что вы хотели, чтобы он делал (ваш пример выше, никакая переменная не будет выделяться, никакой код для перехода, даже при незначительной оптимизации). Или, что еще хуже, вы обнаруживаете ошибки в компиляторе, и ваша программа просто не работает (вот почему -O3 не рекомендуется для gcc). Это и / или вы обнаружите большое количество мест в стандарте C, интерпретация которых определяется реализацией.

Неоптимизированный код легче компилировать, так как он немного более очевиден. В случае вашего примера ожидание, что в стеке размещена переменная, настроено какое-то расположение указателя стека, немедленное значение 1 в итоге записывается в это место, очищается стек и возвращается функция. Компиляторам сложнее ошибиться и более вероятно, что ваша программа работает так, как вы хотели. Обнаружение и удаление мертвого кода - это дело оптимизации и вот где это становится рискованным. Часто риск стоит награды. Но это зависит от пользователя, красота в глазах смотрящего.

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

3 голосов
/ 04 декабря 2010

Скорее всего, здесь происходит то, что Borland вызывает main из своего кода запуска после инициализации всего с кодом, присутствующим в их среде выполнения lib.

Код gcc для меня не выглядит как main, а каксгенерированный код, который вызывает main.Разберите код на sub_401100 и посмотрите, выглядит ли он как ваш основной процесс.

2 голосов
/ 04 декабря 2010

Прежде всего, убедитесь, что вы, по крайней мере, включили флаг оптимизации -O2 для gcc, в противном случае вы вообще не получите никакой оптимизации.

С этим небольшим примером вы не тестируете оптимизацию,Вы видите, как работает инициализация программы, например, gcc вызывает __set_app_type для информирования окон о типе приложения, а также о другой инициализации.например, sub_401100 регистрирует обработчики atexit для среды выполнения.Borland может вызвать инициализацию во время выполнения заранее, в то время как gcc делает это в main ().

0 голосов
/ 12 октября 2012

Разница здесь не в скомпилированном коде, а в том, что вам показывает дизассемблер. Вы можете подумать, что main - единственная функция в вашей программе, но это не так. На самом деле ваша программа выглядит примерно так:

void start()
{
    ... some initialization code here
    int result = main();
    ... some deinitialization code here
    ExitProcess(result);
}

IDA Pro знает, как работает Borland, поэтому он может перейти непосредственно к main , но не знает, как работает gcc, поэтому он показывает вам истинную точку входа в вашу программу. В Borland ASM вы можете видеть, что main вызывается из какой-то другой функции. В GCC ASM вы можете пройти через все эти sub_40xxx , чтобы найти main

0 голосов
/ 05 декабря 2010

Вот разборка main(), которую я получаю из gcc 4.5.1 MinGW в gdb (я добавил return 0 в конце, чтобы GCC не жаловался):

Сначала, когда программа скомпилирована с оптимизацией -O3:

(gdb) set disassembly-flavor intel
(gdb) disassemble
Dump of assembler code for function main:
   0x00401350 <+0>:     push   ebp
   0x00401351 <+1>:     mov    ebp,esp
   0x00401353 <+3>:     and    esp,0xfffffff0
   0x00401356 <+6>:     call   0x4018aa <__main>
=> 0x0040135b <+11>:    xor    eax,eax
   0x0040135d <+13>:    mov    esp,ebp
   0x0040135f <+15>:    pop    ebp
   0x00401360 <+16>:    ret
End of assembler dump.

И без оптимизации:

(gdb) set disassembly-flavor intel
(gdb) disassemble
Dump of assembler code for function main:
   0x00401350 <+0>:     push   ebp
   0x00401351 <+1>:     mov    ebp,esp
   0x00401353 <+3>:     and    esp,0xfffffff0
   0x00401356 <+6>:     sub    esp,0x10
   0x00401359 <+9>:     call   0x4018aa <__main>
=> 0x0040135e <+14>:    mov    DWORD PTR [esp+0xc],0x1
   0x00401366 <+22>:    mov    eax,0x0
   0x0040136b <+27>:    leave
   0x0040136c <+28>:    ret
End of assembler dump.

Они немного сложнее, чем пример Борланда, но не чрезмерно.

Обратите внимание, что вызовы 0x4018aa являются вызовами функции, предоставляемой библиотекой / компилятором для создания объектов C ++. Вот фрагмент из некоторых документов GCC:

Фактические вызовы конструкторов выполняются подпрограммой __main, которая вызывается (автоматически) в начале тела main (при условии, что main был скомпилирован с GNU CC). Вызов __main необходим даже при компиляции кода C, чтобы разрешить связывание объектного кода C и C ++ вместе. (Если вы используете '-nostdlib', вы получите неразрешенную ссылку на __main, так как она определена в стандартной библиотеке GCC. Включите '-lgcc' в конце командной строки вашего компилятора, чтобы разрешить эту ссылку.)

Я не уверен, что именно IDA Pro показывает в ваших примерах. IDA Pro помечает то, что он показывает, как start, а не main, поэтому я предполагаю, что ответ JimR правильный - это, вероятно, инициализация во время выполнения (возможно, точка входа, как описано в заголовке .exe - не main(), а точка входа инициализации во время выполнения).

Понимает ли IDA Pro символы отладки gcc? Вы скомпилировали с опцией -g, чтобы генерировались символы отладки?

0 голосов
/ 04 декабря 2010

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

...