Изучение листинга ассемблирования, генерируемого компилятором C++ - часть 1

ОГЛАВЛЕНИЕ

•    Скачать исходники - 4 Кб

Введение

Компилятор VC++ может создать текстовый файл, показывающий ассемблерный код, сгенерированный для файла C/C++ file. Этот файл позволяет узнать, какой вид кода генерирует компилятор. Файл дает хорошее представление о ряде принципов, таких как обработка исключений, таблицы вызова и т.д. Элементарного знания языка ассемблера достаточно для понимания вывода файла листинга. Цель данной статьи (первой в серии из двух статей) – показать, как файл листинга помогает понять внутренние механизмы компилятора C++.

Установка файла листинга

Можно установить параметры компилятора C/C++ для генерации файла листинга в диалоговом окне «Настройки проекта VC6», как показано ниже.

В VC++.NET можно установить тот же параметр в диалоговом окне «Свойства проекта».

Компилятор генерирует следующие разные типы листинга:

1.    Только ассемблерный код (.asm)
2.    Ассемблерный код и машинный код. (.cod)
3.    Ассемблерный код вместе с исходным кодом. (.asm)
4.    Ассемблерный код вместе с машинным и исходным кодом. (.cod)

Просмотр файла листинга

Изучим листинг, сгенерированный для следующего приложения.

#include <stdio.h>

int main(int argc, char* argv[])
{
    printf("Hello World!");
    return 0;
}       

1.    Листинг только ассемблера (/FA)

Листинг ассемблирования помещается в файл с расширением .asm в промежуточном каталоге. Например, если имя файла - main.cpp, то в промежуточном каталоге появится файл main.asm. Ниже приведен фрагмент кода главной функции из файла листинга:

PUBLIC  _main
PUBLIC  ??_C@_0N@GCDOMLDM@Hello?5World?$CB?$AA@ ; `строка'
EXTRN   _printf:NEAR
; COMDAT ??_C@_0N@GCDOMLDM@Hello?5World?$CB?$AA@
; Файл g:\wksrc\compout\main.cpp
CONST   SEGMENT
??_C@_0N@GCDOMLDM@Hello?5World?$CB?$AA@ DB 'Hello World!', 00H ; `строка'
; Флаги компиляции функции: /Ogty
CONST   ENDS
;   COMDAT _main
_TEXT   SEGMENT
_argc$ = 8
_argv$ = 12
_main   PROC NEAR ; COMDAT
; Строка 5
    push    OFFSET FLAT:??_C@_0N@GCDOMLDM@Hello?5World?$CB?$AA@
    call    _printf
    add esp, 4
; Строка 6
    xor eax, eax
; Строка 7
    ret 0
_main   ENDP
END    

Изучим листинг.

•    Строки, начинающиеся с ; , являются комментариями
•    PUBLIC _main означает, что функция _main используется совместно с другими файлами (в отличие от статических функций). У статических функций нет префикса.
•    CONST SEGMENT указывает начало сегмента данных CONST. Компилятор VC++ помещает в эту секцию постоянные данные, такие как строки. Видно, что строка "Hello World" помещается в сегмент CONST. Изменение любых данных в сегменте вызывает генерацию исключения нарушения доступа. Подробнее об этом позже.
•    _TEXT SEGMENT отмечает начало другого сегмента. Компилятор помещает весь код в этот сегмент.
•    _argc$ = 8 и _argv$ = 12 указывают стековые положения аргументов argc и argv. В данном случае это значит, что если прибавить 8 к указателю стека (регистр ESP процессора), то получится адрес параметра argc. Для адреса возврата будет смещение 4.
•    _main PROC NEAR указывает на начало функции _main. Заметьте, что у функций C (функций, объявленных с extern "C") в начале имени ставится _, у функции C++ имя декорируется.
•    Видно, что компилятор проталкивает адрес строки "Hello World" в стек и вызывает функцию printf. После окончания вызова функции указатель стека увеличивается на 4 (так как printf имеет соглашение о вызовах C).
•    EAX – регистр, хранящий возвращаемое значение функции. EAX подвергается операции "исключающее или" сам с собой. (Это быстрый способ привести регистр к нулю.) Причина состоит в том, что содержимое оригинального кода возвращает 0 из функции main.
•    Наконец, ret 0 – команда возврата из функции. Числовой аргумент 0, идущий за командой ret, указывает число, на которое надо увеличить указатель стека.

Это был листинг только ассемблера. Посмотрим, как выглядят три остальных листинга.

2.    Ассемблер с исходным кодом (/FAs)

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

_TEXT   SEGMENT
_argc$ = 8
_argv$ = 12
_main   PROC NEAR ; COMDAT

; 5    :    printf("Hello World");

    push    OFFSET FLAT:??_C@_0M@KPLPPDAC@Hello?5World?$AA@
    call    _printf
    add esp, 4

; 6    :    вернуть 0;

    xor eax, eax

; 7    : }

3.    Ассемблер с машинным кодом (/FAc)

Листинг показывает коды команд вместе с мнемониками команд. Этот листинг обычно генерируется в файле .cod. В данном примере листинг окажется в файле main.cod.

;   COMDAT 
_main   _TEXT SEGMENT
_argc$  = 8
_argv$ = 12
_main PROC
NEAR  ; COMDAT       
; Строка 5
00000 68 00 00 00 00 push OFFSET    
            FLAT:??_C@_0M@KPLPPDAC@Hello?5World?$AA@
                
00005 e8 00 00 00 00 call _printf   
0000a   83 c4 04 add esp,  4  

; Строка 6
0000d 33  c0    xor eax, eax    

; Строка 7
0000f c3  ret   0       
_main   ENDP

4.    Ассемблер, машинный и исходный код (/FAsc)

Этот листинг также генерируется в файле .cod. Как и ожидалось, он показывает исходный код вместе с машинным кодом и ассемблером.

;   COMDAT _main
_TEXT   SEGMENT
_argc$ = 8
_argv$ = 12
_main   PROC NEAR  ; COMDAT

; 5    :    printf("Hello World");

  00000 68 00 00 00 00   push   
      OFFSET FLAT:??_C@_0M@KPLPPDAC@Hello?5World?$AA@
  00005 e8 00 00 00 00   call    _printf
  0000a 83 c4 04     add     esp, 4

; 6    :    вернуть 0;

  0000d 33 c0        xor     eax, eax

; 7    : }

  0000f c3       ret     0
_main   ENDP

Были рассмотрены все четыре типа листинга, генерируемые компилятором. Обычно нет необходимости смотреть на машинный код. Большей частью ассемблер с исходником (/FAs) – самый полезный листинг.

Посмотрев разные типы листингов и как генерировать листинги, узнаем, какую полезную информацию можно собрать из листинга.

Сегмент Const

Компилятор поместил постоянную строку "Hello World" в сегмент CONST. Последствия этого показаны на следующем пробном приложении.

#include <stdio.h> <stdio.h>

char* szHelloWorld = "Hello World";

int main(int argc, char* argv[])
{
    printf(szHelloWorld);

    szHelloWorld[1] = 'o';
    szHelloWorld[2] = 'l';
    szHelloWorld[3] = 'a';
    szHelloWorld[4] = '\'';

    printf(szHelloWorld);

    return 0;
}

Сначала это пробное приложение печатает "Hello World", пытается преобразовать строку "Hello" в "Hola'" и в конце печатает измененную строку. Скомпонуем и запустим приложение. Оно аварийно завершит работу с исключением нарушения доступа и строкой szHelloWorld[2] = 'l';.

Изменим строку

char* szHelloWorld = "Hello World"; 

на

char szHelloWorld[] = "Hello World"; 

На этот раз приложение успешно запустится. Изучение листинга показывает причину.

1.    В первом случае данные "Hello World" помещаются в сегмент CONST, являющийся неизменяемым сегментом

CONST    SEGMENT
?szHelloWorld@@3PADA DB 'Hello World', 00H ; szHelloWorld
CONST    ENDS               

2.    Во втором случае данные помещаются в сегмент _DATA, являющийся сегментом для чтения и записи

_DATA    SEGMENT
?szHelloWorld@@3PADA DB 'Hello World', 00H ; szHelloWorld
_DATA    ENDS