Различия в дизассемблированном C-коде GCC и Borland?

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

Код Си

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

Борланд АСМ

.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, а затем я перешел к этому коду к другому, и это, кажется, код (правильно ли я в этом предположении, и если почему 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

person Zimm3r    schedule 04.12.2010    source источник


Ответы (6)


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

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

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

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

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

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

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

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

person old_timer    schedule 04.12.2010
comment
Вау, просто вау, спасибо, это очень помогает. :-) Я оставлю вопрос открытым, но пока вы получаете чек и хотя бы один голос "за" :-). - person Zimm3r; 05.12.2010
comment
не спешите проверять ответ, дайте ему несколько дней, несмотря ни на что. Несомненно, есть и другие ответы, достойные проверки, оставьте его открытым, чтобы у них был шанс. - person old_timer; 05.12.2010

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

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

person JimR    schedule 04.12.2010
comment
Похоже, что GCC мог вообще пропустить вызов main(), поскольку он не делает ничего «наблюдаемого». Так что, возможно, main() в gcc даже проще, чем в Borland. - person Michael Burr; 04.12.2010
comment
@Michael Burr: Это возможно ... Я обнаружил, что gcc довольно умен в некоторых вещах. Этому оптимизатору Borland, вероятно, 10 лет, и у него нет последних обновлений :) - person JimR; 04.12.2010
comment
@ Zimm3r: sub_401100 по большей части просто еще один код запуска clib. Если из моего и других комментариев непонятно, Borland помещает свой код запуска в свой clib. Мне кажется, что gcc генерирует код запуска для каждой компиляции. Если у вас есть исходный код Borland clib, посмотрите на startup.asm или startup.s. (Я думаю, что это имя файла в любом случае ... Это были ГОДЫ.) - person JimR; 05.12.2010

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

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

person nos    schedule 04.12.2010
comment
Я включил O2, и это не изменило ничего из приведенного выше дизассемблированного вывода. - person Zimm3r; 04.12.2010
comment
Поскольку этот код представляет собой просто запуск/демонтаж программы, он останется тем же, независимо от того, создаете ли вы систему управления шаттлом или печатаете привет, мир. - person nos; 07.12.2010

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

person Tristan    schedule 04.12.2010
comment
Да, это то, о чем говорится в учебнике, которому я следовал, но тогда почему gcc этого не делает? - person Zimm3r; 04.12.2010

Вот разборка main(), которую я получил от MinGW gcc 4.5.1 в 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.

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

Обратите внимание, что вызовы 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, чтобы генерировались символы отладки?

person Michael Burr    schedule 05.12.2010

Разница тут в основном не в скомпилированном коде, а в том, что вам показывает дизассемблер. Вы можете подумать, что 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, чтобы найти свой основной

person Simon    schedule 12.10.2012