Изучение листинга ассемблирования, генерируемого компилятором C++ - часть 1
ОГЛАВЛЕНИЕ
Введение
Компилятор 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