Code Generation — AST에서 Assembly로
Douglas Thain, Introduction to Compilers and Language Design (2nd ed., 2023) · pp.181–194
📌단원 개요
scanning, parsing, AST 구성, type checking, intermediate representation(IR) 생성을 모두 마친 컴파일러의 마지막 단계. 이제 실제 assembly code를 만든다. 이 장은 naïve approach로, 프로그램의 각 요소를 고립(isolation)시켜 독립적으로 생성한다.
- 각 expression·statement를 이웃을 참조하지 않고 standalone unit으로 생성한다.
- 쉽고 직관적이지만 conservative하여 non-optimal code가 많이 나온다. 하지만 동작은 하며, 다음 장의 optimization을 위한 출발점이 된다.
- 예시는 X86-64 assembly 중심. ARM 등 다른 assembly로 옮기기도 어렵지 않다.
- 이전 단계처럼 요소마다 method 하나를 정의한다:
decl_codegen→stmt_codegen→expr_codegen...
Code Generation 함수 계층 (Figure 11.1)
상위 함수가 하위 함수를 호출하는 구조. decl_codegen이 stmt_codegen을, 그것이 다시 expr_codegen을 호출하고, 최하위에서 symbol_codegen·scratch_alloc·scratch_free 같은 supporting function이 쓰인다.
11.1Introduction
code generation은 컴파일러의 final stage이며, 이 장에서는 가장 단순한 naïve 방식을 먼저 익힌다.
- 각 element를 isolation으로 다룬다 → 구현이 easy & straightforward.
- 대신 conservative하여 large amount of non-optimal code를 생성한다.
- 그래도 동작(work)하며, 더 정교한 기법(다음 장)을 위한 starting point.
- 이전 단계와 동일하게 program element마다 method 하나를 정의하는 패턴을 따른다.
이 장의 입력은 AST 또는 DAG이고, 출력은 X86-64 assembly code다.
11.2Supporting Functions
본격적인 code generation 전에, 세부 사항을 추적할 supporting function들을 먼저 만든다: scratch register 관리, label 생성, symbol 주소 계산.
① Scratch Register 관리
expression을 생성하려면 operator 사이의 intermediate value를 담을 scratch register가 필요하다. 다음 인터페이스의 세 함수를 작성한다.
int scratch_alloc();
void scratch_free( int r );
const char * scratch_name( int r );
Chapter 10에서 각 register는 용도가 정해져 있었다: 일부는 function argument용, 일부는 stack frame management용, 나머지가 scratch value용이다. scratch register들을 다음과 같은 table로 관리한다.
| r | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| name | %rbx | %r10 | %r11 | %r12 | %r13 | %r14 | %r15 |
| inuse | X | X |
scratch_alloc: table에서 unused register를 찾아 in use로 표시하고 register numberr을 반환.scratch_free: 해당 register를 다시 available로 표시.scratch_name: numberr로 register의 이름(%rbx등)을 반환.- scratch register가 고갈(run out)될 수 있으나 드물다. 부족하면 일단 error message 출력 후 halt.
② Label 생성
jump와 conditional branch의 target이 될 unique, anonymous label을 대량으로 생성해야 한다. 두 함수를 작성한다.
int label_create();
const char * label_name( int label );
label_create: global counter를 증가시켜 현재 값을 반환.label_name: 그 label을 string 형태로 반환. 예를 들어 label 15는".L15"로 표현된다.
③ Symbol 주소 계산: symbol_codegen
프로그램의 symbol을 그 symbol을 나타내는 assembly code로 mapping해야 한다. symbol의 주소를 생성하는 함수를 작성한다.
const char * symbol_codegen( struct symbol *s );
이 함수는 주어진 symbol에 필요한 address computation을 나타내는 instruction fragment 문자열을 반환한다. 먼저 symbol의 scope를 검사한다.
| scope | 처리 |
|---|---|
| Global variable | 쉽다. assembly의 이름이 source 언어의 이름과 동일. count:integer라면 symbol_codegen은 그냥 count를 반환. |
| Local variable / Parameter | stack 상의 위치를 가리키는 address computation을 반환. typechecking 단계에서 각 parameter·local variable에 부여한 unique sequence number를 이용한다. |
f: function void ( x: integer, y: integer ) =
{
z: integer = 10;
return x + y + z;
}
x= parameter position 0 → 주소-8(%rbp)y= parameter position 1 → 주소-16(%rbp)z= local variable 0 → 주소-24(%rbp)- 즉 stack frame 내 position만 알면
symbol_codegen이 정확한 stack address 문자열을 반환할 수 있다.
11.3Generating Expressions
expression의 assembly code 생성의 기본 방식은 AST 또는 DAG의 post-order traversal을 수행하며 각 node마다 instruction을 하나 이상 emit하는 것이다.
- 각 intermediate value가 어느 register에 있는지 추적하는 것이 관건.
- AST/DAG node 구조에
regfield를 추가하여scratch_alloc이 반환한 register number를 담는다. - 각 node 방문 시 instruction을 emit하고, 그 값을 담은 register number를
reg에 저장. - node가 더 이상 필요 없으면
scratch_free로 register를 해제한다.
예시: DAG로부터 X86 code 생성 (Figure 11.2)
아래 DAG는 c = (a+3) - b를 표현 (a, b, c는 global integer).
생성되는 instruction 순서 (R0, R1 = scratch register)
1. MOVQ a, R0
2. MOVQ $3, R1
3. ADDQ R0, R1
4. MOVQ b, R0
5. SUBQ R0, R1
6. MOVQ R1, c
Post-order traversal의 단계별 동작
- a node:
scratch_alloc으로 register 0 할당,node->reg에 저장.MOVQ a, R0emit. - 3 node: register 1 할당.
MOVQ $3, R1emit. - IADD node: 두 child가 R0, R1에 있으므로
ADDQ R0, R1emit. 이는 destructive two-address instruction으로 결과가 R1에 남는다. R0는 불필요 →scratch_free(0). - b node: register 0 할당.
MOVQ b, R0emit. - ISUB node:
SUBQ R0, R1emit. 결과 R1, R0 해제. - c node: assignment의 target이므로 아무것도 emit하지 않음.
- ASSIGN node:
MOVQ R1, cemit.
R0 = scratch_name(0) = %rbx, R1 = %r10 등. 같은 코드를 실제 이름으로 쓰면:
MOVQ a, %rbx
MOVQ $3, %r10
ADDQ %r10, %rbx
MOVQ b, %rbx
SUBQ %rbx, %r10
MOVQ %r10, c
expr_codegen 구현 골격 (Figure 11.3)
먼저 left·right child에 대해 재귀적으로 expr_codegen을 호출한다. 각 child가 결과를 자신의 reg field에 남기면, 현재 node가 그 register들을 이용해 code를 생성하고 더 이상 필요 없는 register를 free한다.
void expr_codegen( struct expr *e )
{
if(!e) return;
switch(e->kind) {
// Leaf node: allocate register and load value.
case EXPR_NAME:
e->reg = scratch_alloc();
printf("MOVQ %s, %s\n",
symbol_codegen(e->symbol),
scratch_name(e->reg));
break;
// Interior node: generate children, then add them.
case EXPR_ADD:
expr_codegen(e->left);
expr_codegen(e->right);
printf("ADDQ %s, %s\n",
scratch_name(e->left->reg),
scratch_name(e->right->reg));
e->reg = e->right->reg;
scratch_free(e->left->reg);
break;
case EXPR_ASSIGN:
expr_codegen(e->left);
printf("MOVQ %s, %s\n",
scratch_name(e->left->reg),
symbol_codegen(e->right->symbol));
e->reg = e->left->reg;
break;
. . .
}
}
기본 과정에 필요한 보완 (refinements)
symbol이 instruction의 일부가 될 때는 symbol_codegen으로 해당 주소 문자열을 받아야 한다. 예를 들어 a가 function의 first parameter였다면 첫 instruction은 이렇게 된다.
MOVQ -8(%rbp), %rbx
- X86
IMUL은 argument를 하나만 받는다. 첫 argument는 항상%rax, 결과도 항상%rax에 들어가고 overflow는%rdx로 간다. - 따라서 한 child register를
%rax로 옮기고, 다른 child register로 multiply한 뒤, 결과를%rax에서 destination scratch register로 옮긴다. %rax와%rdx는 multiply 진행 중 다른 용도로 쓸 수 없다. scratch register가 많으므로 basic generator에서는%rdx를 아예 다른 용도로 쓰지 않는다.
MOV $10, %rbx
MOV x, %r10
MOV %rbx, %rax
IMUL %r10
MOV %rax, %r11
보완 3 — function 호출 (Figure 11.4)
function은 단일 CALL node로 표현되고, 각 argument는 ARG node의 unbalanced tree로 표현된다. 아래는 a=f(10,b+c)를 표현하는 DAG이다.
생성 코드 (register calling convention)
MOVQ $10, %rbx
MOVQ b, %r10
MOVQ c, %r11
ADDQ %r10, %r11
MOVQ %r11, %rsi
MOVQ %rbx, %rdi
PUSHQ %r10
PUSHQ %r11
CALL f
POPQ %r11
POPQ %r10
MOVQ %rax, %rbx
MOVQ %rbx, a
- 각
ARGnode를 평가하여 left hand child의 값을 계산한다. - Stack calling convention: 각
ARGnode가 stack으로의PUSH하나에 대응. - Register calling convention: 모든 argument를 먼저 생성한 뒤, 생성 완료 후에 각각을 알맞은 argument register로 복사.
- caller-saved register를 저장한 뒤 function name에
CALLemit. - 호출 반환 시 return register(
%rax)의 값을 새로 할당한 scratch register로 옮기고, caller-saved register를 복원.
- 모든 expression은 scratch register에 남겨 parent가 참조할 value를 가진다.
- 일부 expression은 value 외에 추가 동작인 side effect도 가진다. 둘 중 하나를 빠뜨리기 쉽다!
- 예:
(x=10)은 value 10을 산출 → value가 필요한 곳 어디든 쓸 수 있어y=x=10이나f(x=10)이 가능. 동시에 10을 x에 저장하는 side effect도 가진다. - 따라서
x=10생성 시 (1) 10을 x에 저장하는 side effect 수행 + (2) value 10을 register에 유지 둘 다 해야 한다.
11.4Generating Statements
expression 생성을 expr_codegen에 캡슐화했으니, 이를 기반으로 더 큰 code 구조를 만든다. stmt_codegen이 모든 control flow statement의 code를 만든다.
void stmt_codegen( struct stmt *s )
{
if(!s) return;
switch(s->kind) {
case STMT_EXPR:
...
break;
case STMT_DECL:
...
break;
...
}
stmt_codegen(s->next);
}
마지막에 stmt_codegen(s->next)를 호출하여 statement list를 순차적으로 처리한다.
쉬운 경우들
STMT_DECL — local variable 선언
case STMT_DECL:
decl_codegen(s->decl);
break;
decl_codegen에 위임한다.
STMT_EXPR — expression statement
case STMT_EXPR:
expr_codegen(s->expr);
scratch_free(s->expr->reg);
break;
expr_codegen 호출 후 top-level value를 담은 scratch register를 free. (사실 expr_codegen 호출마다 register 하나가 free되어야 한다.)
return statement
expression을 평가하여 return value 전용 register %rax로 옮기고, stack을 풀고 호출 지점으로 돌아가는 function epilogue로 jump한다.
case STMT_RETURN:
expr_codegen(s->expr);
printf("MOV %s, %%rax\n",scratch_name(s->expr->reg));
printf("JMP .%s_epilogue\n",function_name);
scratch_free(s->expr->reg);
break;
이 code는 statement를 포함하는 function의 이름을 알아야 한다(function_name). 그 정보를 아래로 전달할 방법을 마련해야 한다.
if statement (conditional)
원하는 출력 assembly를 먼저 생각하고 거꾸로 생성기를 작성하는 것이 유용하다. control expression을 평가해 값이 known register에 있게 하고, CMP로 0(false)인지 검사한다. false면 JE(jump-if-equal)로 false branch로 jump, 아니면 true branch로 진행. true statement 끝에서 JMP로 else body를 건너뛴다.
assembly template
expr
CMP $0, register
JE false-label
true-statements
JMP done-label
false-label :
false-statements
done-label :
STMT_IF generator
case STMT_IF:
int else_label = label_create();
int done_label = label_create();
expr_codegen(s->expr);
printf("CMP $0, %s\n",scratch_name(s->expr->reg));
scratch_free(s->expr->reg);
printf("JE %s\n",label_name(else_label));
stmt_codegen(s->body);
printf("JMP %s\n",label_name(done_label));
printf("%s:\n",label_name(else_label));
stmt_codegen(s->else_body);
printf("%s:\n",label_name(done_label));
break;
for loop
source template: for ( init-expr ; expr ; next-expr ) { body-statements }. init-expr을 먼저 평가하고, 매 iteration마다 control expression을 평가한다. false면 loop 끝으로 jump, 아니면 body 실행 후 next-expression 평가.
init-expr
top-label:
expr
CMP $0, register
JE done-label
body-statements
next-expression
JMP top-label
done-label :
- 세 component는 모두 그냥 expression이다. 관습적으로 첫째는 초기화 side effect(
i=0), 둘째는 비교(i<10), 셋째는 다음 값 생성 side effect(i++). - 세 expression 각각은 생략(omit) 가능. init-expr·next-expr 생략 시 효과 없음. expr(control) 생략 시 true로 간주.
continue;·break;: 현재 loop의 label을 추적하여 각각 top label·done-label로의JMP로 변환.
print statement (B-Minor)
출력할 expression의 type에 따라 동작이 달라지는 imperative statement의 특수 사례. integer·boolean·string마다 약간 다른 code가 필요하다.
source
i: integer = 10;
b: boolean = true;
s: string = "\n";
print i, b, s;
동등한 처리 (function call로 환원)
print_integer(i);
print_boolean(b);
print_string(s);
- integer 출력에 직접 대응하는 단순 assembly는 없다 → 이미 아는 추상화인 function call로 환원하는 것이 흔한 방법.
- print statement 생성: 각 expression의 code를 생성 →
expr_typecheck로 type 판정 → 대응하는 function call emit. - 이 함수들은 작성되어 각 B-Minor 프로그램에 link되어야 한다. 이들과 기타 필요한 함수들을 통칭 runtime library라 한다.
- 일반 규칙: 언어가 high-level일수록 더 많은 runtime support가 필요.
11.5Conditional Expressions
conditional expression(less-than, greater-than, equals 등)은 두 값을 비교해 boolean을 반환한다. control flow에 흔히 나타나지만 단순 값으로도 쓸 수 있다(예: b: boolean = x < y;).
비교를 수행해 boolean 결과를 register에 넣는 single instruction이 없다. 따라서 control flow 구조를 만들어 두 expression을 비교하고 원하는 결과를 구성하는 우회로를 택해야 한다.
conditional expression 생성 template
left-expr < right-expr에 대한 생성 코드:
left-expr
right-expr
CMP left-register, right-register
JLT true-label
MOV false, result-register
JMP done-label
true-label:
MOV true, result-register
done-label:
- different conditional operator에는 알맞은 자리에 different jump instruction을 사용한다.
- 약간의 변형으로 ternary conditional operator
(x?a:b)도 같은 방식으로 구현 가능.
if(x>y){...}를 obvious하게 생성하면 assembly에 conditional 구조가 둘 생긴다.- 첫째 conditional:
x>y결과를 register에 계산. - 둘째 conditional: 그 결과를 0과 비교해 true/false branch로 jump.
- 이 common case를 검사하면, expression을 평가하고 conditional jump 하나로 statement를 구현하는 단일 conditional로 최적화할 수 있다.
11.6Generating Declarations
전체 프로그램 emit은 각 declaration(code 또는 data)을 순회하며 기본 구조를 내보내는 일이다. declaration은 세 경우로 나뉜다.
(B-Minor는 local function declaration을 허용하지 않는다.)
① Global data declaration
label과 함께 적절한 directive로 공간을 예약하고, 필요시 initializer를 emit한다.
B-Minor (global scope)
i: integer = 10;
s: string = "hello";
b: array [4] boolean = {true, false, true, false};
출력 directives
.data
i: .quad 10
s: .string "hello"
b: .quad 1, 0, 1, 0
global variable declaration은 constant value로만 초기화 가능하고 general expression으로는 불가능하다. data section은 constant만 담을 수 있고 code는 못 담기 때문. 만약 초기화에 code를 넣었다면 code generation 전에 typechecker가 error를 발견했어야 한다.
② Local variable declaration
stmt_codegen이 function declaration 내부에서decl_codegen을 호출할 때만 발생.- local variable 공간은 이미 function prologue가 확보했다고 가정 → stack 조작 불필요.
- 단, initializing expression이 있으면(
x:integer=y*10;) expression code를 생성하고 local variable에 저장한 뒤 register를 free한다.
③ Function declaration (마지막 조각)
- label: function 이름으로 emit.
- prologue: parameter 수와 local variable 수를 고려해 stack에 적절한 공간을 만든다.
- body: function 본문.
- epilogue: return statement가 쉽게 jump할 수 있도록 unique label을 가져야 한다.
11.7Exercises (연습문제)
- 이 장의 기법으로 했을 때 여섯 개의 scratch register를 모두 소진시키는 legal expression을 작성하라. 일반적으로 임의의 expression tree의 code 생성에 register는 몇 개 필요한가?
- register calling convention 사용 시, 값을 argument register로 옮기기 전에 모든 function argument의 값을 먼저 생성해야 하는 이유는?
- global variable declaration이 non-constant initializing expression을 가질 수 있는가? 이유를 설명하라.
- B-Minor에 switch statement가 있다고 가정하고, switch 구현을 위한 두 가지 다른 assembly template을 스케치하라.
- 이 장에서 설명한 대로 X86-64 architecture용 완전한 code generator를 작성하라.
- B-Minor의 모든 측면을 시험하는 여러 test program을 작성하고, 컴파일러로 build·test·run하라.
- 여러분의 컴파일러 출력을 gcc 같은 production compiler의 동등한 C 프로그램 출력과 비교하라. 어떤 차이가 보이는가?
- ARM 같은 다른 assembly나 LLVM 같은 IR을 emit하는 추가 code generator를 더하라. 어떤 접근 변화가 필요했는지 기술하라.
⭐핵심 정리 / 시험 포인트
- code generation은 컴파일러의 final stage. 이 장은 element를 isolation으로 다루는 naïve approach → 쉽지만 non-optimal code.
- 함수 계층:
decl_codegen→stmt_codegen→expr_codegen. supporting:symbol_codegen,scratch_alloc/free/name,label_create/name. - expression 생성 = post-order traversal. 각 node의 결과 register를
regfield에 저장. - scratch register table: r0=%rbx, r1=%r10, … r6=%r15 (총 6~7개). 부족하면 error 후 halt.
label_name(15)→".L15"형태 string.- symbol scope별 주소: global = 이름 그대로, local/parameter =
-N(%rbp)(param0=-8, param1=-16, local0=-24 ...). - X86
IMUL은 argument 하나만 받음 →%rax가 첫 인자·결과, overflow는%rdx. basic generator는%rdx를 다른 용도로 안 씀. - function call:
CALLnode +ARGnode의 unbalanced tree. register convention은 모든 argument 생성 후 argument register로 복사. return은%rax. - expression은 value + side effect를 모두 가질 수 있다(예:
x=10은 값 10 + x에 저장). - if =
CMP $0+JE; for의 control expression 생략 시 true로 간주;break/continue는 done-label/top-label로JMP. - conditional expression은 단일 instruction이 없다 → CMP + 조건부 jump로 control flow를 만들어 boolean 구성.
- global data는 constant로만 초기화(data section엔 code 불가, typechecker가 검출).
- function = label → prologue → body → epilogue. epilogue는 unique label.
- print는 type별 function call로 환원 → runtime library 필요. high-level일수록 runtime support↑.
r0 | %rbx |
r1 / r2 | %r10 / %r11 |
r3 / r4 | %r12 / %r13 |
r5 / r6 | %r14 / %r15 |
%rax | return value · IMUL 결과 |
%rdx | IMUL overflow (basic에선 미사용) |
- expression = post-order (children → node).
- load leaf:
MOVQ symbol, reg - add:
ADDQ, sub:SUBQ(destructive 2-address). - compare:
CMP, jump:JE / JLT / JMP. - directive:
.data / .quad / .string.
- Q. naïve code generation의 장단점은? → 쉽고 직관적(element를 isolation으로 처리) / conservative하여 non-optimal code 다량 생성.
- Q.
f(x:integer,y:integer)내 localz의 stack 주소는? → x=-8(%rbp), y=-16(%rbp), z=-24(%rbp). - Q. X86 IMUL이 까다로운 이유? → argument 하나만 받고
%rax를 강제 사용, overflow는%rdx→ 추가 MOV들 필요. - Q. conditional expression에 instruction 하나로 안 되는 이유? → 비교 결과를 register에 바로 넣는 single instruction이 없어 control flow(CMP+jump)로 우회.
- Q. global variable이 expression으로 초기화 못 하는 이유? → data section은 constant만 담고 code는 못 담음(typechecker가 사전 검출).
- Q. runtime library란? → print 등 type별 동작을 수행하는 함수 모음. 각 프로그램에 link됨. high-level일수록 더 필요.
📋한눈에 보기 — Cheat Sheet
| 키워드 / 함수 | 의미 |
|---|---|
| decl_codegen | declaration 생성 (최상위 진입) |
| stmt_codegen | control flow statement 생성, 끝에서 s->next 재귀 |
| expr_codegen | expression 생성. post-order, 결과를 reg field에 |
| symbol_codegen | symbol 주소 fragment 반환 (global=이름, local=-N(%rbp)) |
| scratch_alloc / free / name | scratch register 할당 / 해제 / 이름 |
| label_create / label_name | unique label 번호 생성 / ".L15" string |
| reg field | node 값이 담긴 register number 저장 |
| %rbx,%r10–%r15 | scratch register (r0~r6) |
| %rax | return value, IMUL 첫 인자·결과 |
| %rdx | IMUL overflow (basic generator 미사용) |
| %rdi,%rsi,… | argument register (register calling convention) |
| %rbp | frame pointer, 지역 주소 -N(%rbp) 기준 |
| MOVQ / ADDQ / SUBQ | load·move / 덧셈 / 뺄셈 (destructive 2-address) |
| IMUL | 1개 인자, %rax·%rdx 사용 |
| CMP / JE / JLT / JMP | 비교 / equal분기 / less-than분기 / 무조건분기 |
| PUSHQ / POPQ | caller-saved register 저장·복원, stack arg |
| CALL | function 호출 node / instruction |
| ARG node | argument의 unbalanced tree |
| .data / .quad / .string | global data directive |
| prologue / epilogue | stack 공간 확보 / 복원 (epilogue=unique label) |
| runtime library | print_integer 등 type별 지원 함수 모음 |
| side effect | value 외 추가 동작 (예: x=10의 저장) |
| conditional expression | CMP+jump로 boolean 구성 (단일 instruction 없음) |