С gcc, например, я думаю, что опция для использования -save-temps.
Грубо говоря, необходимо выполнить передачу файла для извлечения всех включений и создать по существу один файл для анализа.Многие инструменты в наши дни используют синтаксический анализатор, работающий по набору правил (bison, yacc, flex и т. Д.), Целью которого является синтаксический анализ ascii, превращающий вашу программу в своего рода очень широкий язык ассемблера за отсутствием лучшего термина.
a = a + 1;
может превратиться в
Load variable named a, size of blah, type unsigned foo
load immediate 1, size blah, unsigned
add
store result a
Затем возможны оптимизации, промежуточный язык компилятора может иметь функцию приращения и определять, что приращение лучше, чем загрузка 1и доп.В конце концов, эти оптимизации завершены, и этот промежуточный код проходит через бэкэнд к целевому набору команд.Обычно это выводится как сборка и подается в ассемблер, который превращает его в объектный файл, и может произойти целевая оптимизация.Затем объектные файлы подаются в компоновщик, который, ну, в общем, связывает их вместе.Одна функция в одной программе может вызывать функцию, которая не находится в этом объектном файле с именем bob, объектный файл не имеет адреса или смещения для достижения bob, он оставляет там дыру для адреса, который нужно вставить, и задача компоновщиков состоит в том, чтобы соединить всеиз них определите, где в двоичном файле будет жить функция bob (назначьте ей адрес), затем найдите все места, которые вызывают bob, и, когда они помещены в память, вставьте инструкцию или адрес, необходимые для вызова bob, чтобыконечный результат - исполняемый двоичный файл.
llvm, который уже является конкурентом gcc, обеспечивает хороший обзор этого процесса.Вы можете иметь код на C, скомпилированный в промежуточное звено.Начните с нашей функции bob
unsigned int bob ( unsigned int a )
{
return(a+1);
}
скомпилируйте в битовый код
clang -c -o bob.bc -emit-llvm bob.c
разберите битовый код в удобочитаемую форму
llvm-dis bob.bc
В результате bob.ll
define i32 @bob(i32 %a) nounwind {
entry:
%a.addr = alloca i32, align 4
store i32 %a, i32* %a.addr, align 4
%tmp = load i32* %a.addr, align 4
%add = add i32 %tmp, 1
ret i32 %add
}
Неоптимизируемый код любит часто храниться и извлекаться из памяти, и при частой передаче в функцию, сохраняемую и извлекаемую из стека.
В дополнение к тому, что вы легко можете видеть за кулисами,llvm хорош, потому что вы можете оптимизировать на любом уровне, комбинировать объекты и оптимизировать на всем уровне программы, где gcc ограничит вас только уровнем файлов или функций.Таким образом, мы можем оптимизировать этот битовый код.
opt -std-compile-opts bob.bc -o bob_opt.bc
llvm-dis bob_opt.bc
И эти дополнительные хранилища и загрузки исчезли, и ядро функции осталось.
define i32 @bob(i32 %a) nounwind readnone {
entry:
%add = add i32 %a, 1
ret i32 %add
}
Затем llc используется для превращения этого в ассемблердля желаемой цели
llc -march=arm bob.bc
cat bob.s
...
bob: @ @bob
@ BB#0: @ %entry
str r0, [sp, #-4]!
add r0, r0, #1
add sp, sp, #4
bx lr
...
llc -march=arm bob_opt.bc
cat bob_opt.s
...
bob: @ @bob
@ BB#0: @ %entry
add r0, r0, #1
bx lr
...
Да, есть много книг.И много-много компиляторов и т. Д. В дополнение к llvm, у Фабриса Белларда (да, сотрудника qemu) есть супер простой компилятор, который создает промежуточный файл, который вы можете исследовать http://bellard.org/fbcc/, который скрыт так, что онвряд ли, интересно смотреть, хотя, если вы только начинаете понимать внутренности компиляторов.Кроме того, есть еще один хорошо известный tcc http://bellard.org/tcc/, в котором, в частности, отсутствует серверная часть, проходящая через ассемблер, коды операций генерируются непосредственно как для скорости, так и для (ре) компиляции в реальном времени.