Как работает компилятор?
Но сначала два слова о техническом моменте: мы думаем, что приводить скриншоты будем только в самом крайнем случае, так как они отвлекают от чтения, а особо ценной информации не несут: вместо них будем использовать укороченные листинги программ из программы Hiew. Если вы ее еще не нашли, самое время это сделать.
А теперь ответы по двум вопросам из первой части: как компилятор TURBO Pascal 7.1 организует хранение массивов:
VAR m: ARRAY [1..9] of INTEGER; BEGIN M[1]:=2; M[2]:=6; M[3]:=5*3; M[4]:=5*1; M[5]:=5*2; M[8]:=2+2; M[9]:=M[1]+M[8]-M[2]; END.
05F: mov w,[00050],0002 065: mov w,[00052],0006 06B: mov w,[00054],000F 071: mov w,[00056],0005 077: mov w,[00058],000A 07D: mov w,[0005E],0004 083: mov ax,[00050] 086: add ax,[0005E] 08A: sub ax,[00052] 08E: mov [00060],ax
Комментарии здесь почти не требуются. Видно, что массив располагается последовательно, начиная с ячейки 50, под каждую переменную отводятся два байта. По командам с адреса 6B по 7D видно, что компилятор Паскаля способен вычислять выражения с известными заранее константами. Последнее выражение предсказуемо разбивается на серию команд – мы уже говорили, что в Intel-процессорах обмен с памятью возможен только через регистр. В данном случае, через регистр ax. В компьютерах серии DEC (VAX, LSI-11, PDP-11, ДВК) 80-90-х годов, кстати, была более прогрессивная система команд. Собственно, хуже системы команд и архитектуры Intel найти просто невозможно, но это к слову.
Второй вопрос, оставшийся без рассмотрения, касается последних строк программы:
Xor ax, ax Call [xxxx]:Смещение_к_концу_программы
Как вы помните, команда xor очищает регистр. Зачем это? Если вы когда-нибудь работали с командными файлами bat в MS-DOS, то знаете, что после окончания программа может сообщить о результате своего завершения, выдав код ошибки: 0 – нормальное завершение, иначе нужно анализировать конкретный код и принимать меры. Вот так: компилятор ничего так просто не делает, все имеет смысл.
Интересно, как Паскаль работает с регистрами, которые объявляются в программе и служат для организации резидентных программ, программ обработчиков прерываний.
Uses Dos; Var R: Registers; Begin r.bx:=$CC; r.dx:=$DD; intr($21, r); {такой функции нет, программу не запускать!} End.
05F: mov w,[00052],00CC 065: mov w,[00056],00DD 06B: mov al,21
Как видно, запись в “регистры” сначала осуществляется в ячейки памяти, затем настраивается вызов функции 21 ОС. Далее, и это не показано, все это записывается в стек, чтобы библиотечная функция сохранила реальные регистры, перезагрузила их, вызвала функцию, восстановила регистры и вернула управление основной программе. Довольно неожиданно – компилятор защищает программу от прямых манипуляций с системой.
Передача параметров в процедуру или функцию – тоже интересная вещь. В частности, направление передачи у Паскаля и Си различаются. Кроме того, паскалевская процедура сама после выполнения работы корректирует вершину стека, а “сишная” программа занимается этим перед вызовом функций.
var a, b, c, d: integer;
procedure Proc (a, b, c: integer); begin d:=a+b+c; end;
begin Proc(1,2,3); writeln(d); end.
; в Паскале параметры передаются слева направо, т.е. последний будет выше всех в стеке 089: mov ax,0001 08C: push ax 08D: mov ax,0002 090: push ax 091: mov ax,0003 094: push ax ; ближний вызов процедуры; а нам говорили, что это будет похоже на GOTO – ничего подобного! 095: call 00000060
; а вот и сама процедура 060: push bp ; сохранить фрейм стека 061: mov bp,sp ; настроить указатель стека 063: xor ax,ax 065: call 0006:02CD ; интересно, зачем вызывается эта подпрограмма? Пока не знаем. 06A: mov ax,[bp][00008] ; a 06D: add ax,[bp][00006] ; b 070: add ax,[bp][00004] ; c 073: mov [00058],ax ; d:=a+b+c; 076: pop bp 077: retn 0006 ; убрать из стека 6 байтов, занятых нашими переменными
Казалось бы, данный пример не представляет особого интереса, но это не так. Как вы понимаете, на этапе генерации кода порядок записи параметров в стек – единственное, что отличает разные языки и компиляторы. Если изменить порядок и зачистку стека, то различия вызовов подпрограмм исчезают. В той же Windows компиляторы уже давно научились использовать различные стили записи в стек при вызове процедур: в стиле Си, Паскаля и т.д. Подробнее можно почитать в ru.wikipedia.org/wiki/Соглашение_вызова.
В следующей части мы продолжим исследования работы компиляторов. |