или линкером (англ.: linker), объединяет полученный объектный код с объектным кодом библиотек и других файлов, в результате чего получается готовая к исполнению программа. На практике, большинство компиляторных пакетов выполняют все три шага: компиляцию, ассемблирование и компоновку. Наконец, загрузчик загружает программу в память и запускает ее. В оставшейся части этого раздела мы более подробно рассмотрим эти этапы на примере простой программы.
Рис. 2.22. Этапы трансляции и запуска программы
Пример Компиляция программы
Код на языке высокого уровня int f, g, y; // global variables
int main(void)
{
f = 2; g = 3;
y = sum(f, g); return y;
96
} |
|
|
int sum(int a, int b) { |
||
return (a + b); |
|
|
} |
|
|
Код на языке ассемблера MIPS |
||
.data |
|
|
f: |
|
|
g: |
|
|
y: |
|
|
.text |
|
|
main: |
|
|
addi $sp, $sp, −4 # make stack frame |
||
sw |
$ra, 0($sp) |
# store $ra on stack |
addi $a0, $0, 2 |
# $a0 = 2 |
|
sw |
$a0, f |
# f = 2 |
addi $a1, $0, 3 |
# $a1 = 3 |
|
sw $a1, g |
|
# g = 3 |
jal sum |
# call sum function |
|
sw |
$v0, y |
# y = sum(f, g) |
Iw |
$ra, 0($sp) |
# restore $ra from stack |
addi $sp, $sp, 4 |
# restore stack pointer |
|
jr |
$ra |
# return to operating system |
sum: |
|
|
|
add |
$v0, $a0, $a1 # $v0 = a + b |
jr $ra |
|
# return to caller |
Этап 1: Компиляция Компилятор транслирует код высокого уровня в код на
языке ассемблера. В примере кода вышеизложенного кода показана простая программа на языке высокого уровня, содержащая три глобальные переменные и две функции, а также ассемблерный код, сгенерированный типичным компилятором. Ключевые слова .data и .text – это ассемблерные директивы, указывающие на начало сегментов данных и кода соответственно. Для обозначения глобальных переменных f, g и y использу-
97
ются метки. Места для их хранения будут определены ассемблером. На данный момент они остаются в коде в виде символов.
Этап 2: Трансляция Ассемблер транслирует код на языке ассемблера в объ-
ектный файл, содержащий код на машинном языке. Для трансляции кода ассемблер делает два прохода. Во время первого прохода ассемблер назначает адреса командам и находит все символы, такие как метки и имена глобальных переменных. После первого прохода ассемблера код выглядит следующим образом:
0x00400000 main: addi $sp, $sp, −4
0x00400004 |
sw |
$ra, 0($sp) |
0x00400008 |
addi $a0, $0, 2 |
|
0x0040000C |
sw |
$a0, f |
0x00400010 |
addi $a1, $0, 3 |
|
0x00400014 |
sw |
$a1, g |
0x00400018 |
jal sum |
|
0x0040001C |
sw |
$v0, y |
0x00400020 |
lw |
$ra, 0($sp) |
0x00400024 |
addi $sp, $sp, 4 |
|
0x00400028 |
jr |
$ra |
0x0040002C sum: add $v0, $a0, $a1 0x00400030 jr $ra
Имена и адреса символов хранятся в таблице символов. Таблица символов для нашего примера приведена в табл. 2.2. Адреса символов заполняются после первого прохода, когда адреса меток уже известны.
Глобальным переменным присваиваются адреса из сегмента глобальных данных, начиная с адреса 0x10000000. Во время второго прохода ассемблер генерирует машинный код.
98
Адреса глобальных переменных и меток берутся из табл. 2.2 символов.
Код на машинном языке и таблица символов сохраняются в объектном файле.
|
|
Таблица 2.2 |
|
Таблица символов |
|
Символ |
|
Адрес |
f |
|
0x10000000 |
g |
|
0x10000004 |
y |
|
0x10000008 |
main |
|
0x00400000 |
sum |
|
0x0040002C |
Этап 3: Компоновка Большие программы обычно содержат много файлов.
Если программист изменяет один из этих файлов, то перекомпилировать и заново транслировать все остальные файлы выходит довольно затратно. Например, программы часто вызывают функции из библиотечных файлов, которые почти никогда не меняются, а соответствующие объектные файлы не нуждаются в обновлении.
Работа компоновщика заключается в том, чтобы объединить все объектные файлы в один-единственный файл с машинным кодом, который называется исполняемым файлом. Компоновщик перемещает данные и команды в объектных файлах так, чтобы они не наслаивались друг на друга. Он использует информацию из таблицы символов для коррекции адресов перемещаемых глобальных переменных и меток.
В нашем примере только один объектный файл, поэтому никакого перемещения не требуется. На рис. 2.23 показан полученный исполняемый файл.
99
Рис.2.23. Исполняемый файл
Он состоит из трех секций: заголовка, сегмента кода и сегмента данных. Заголовок исполняемого файла содержит информацию о размерах сегмента кода (т.е. об объеме кода) и размерах сегмента данных (т.е. о количестве глобально объявленных данных). Все размеры приведены в байтах. Команды в сегмент кода приведены в том же порядке, в котором они расположены в памяти.
На рисунке рядом с машинным кодом показаны команды в виде, удобном для восприятия и интерпретации человеком. Исполняемый файл содержит только машинные команды. Сегмент данных задает адреса всех глобальных переменных. Доступ к глобальным переменным осуществляется при помощи базовой адресации относительно адреса, определяемого глобальным указателем $gp.
Например, первая команда sw $a0, 0x8000 ($gp) присваивает значение 2 глобальной переменной f, которая размещена в памяти по адресу 0x10000000. Помните, что смещение 0x8000
– это 16-битное число со знаком, которое после знакового расширения до 32 битов прибавляется к базовому адресу, находящемуся в регистре $gp. Таким образом, $gp + 0x8000 =
100