Code Generation — শেষ ধাপে সিলিকনের ভাষায় অনুবাদ
Code Generation
Register allocation শেষ হলে code generator টার্গেট ISA-র প্রকৃত instruction লেখে — frame layout ঠিক করে, prologue/epilogue emit করে, calling convention মেনে — আর এই লেসনে আমরা toy compiler-এর পুরো ধাপটা হাতে করে করে দেখি।
আগে এটা বুঝি
Module-এর শুরু (compiler-pipeline-overview) থেকে যাত্রা: lex → token, parse → AST, নেমে আসে IR; optimizer পরিষ্কার করে, register allocator ঠিক করে দেয় কোন value register-এ, কোনটা spill।
অথচ আউটপুট এখনো কাগজে-কলমে নকশা — TAC, SSA, interference graph, CPU কোনোটাই চালাতে পারে না। CPU চেনে শুধু নিজের ISA-র opcode — fetch-decode-execute unit যা decode করতে পারে (what-is-an-isa lesson)।
আজকের লেসনে সেই শেষ অনুবাদটা — code generation, backend-দের ভাষায় instruction emission: IR-এর প্রতিটা operation concrete machine instruction-এ, প্রতিটা virtual register তার allocated home-এ, ফাংশনের চারপাশে prologue-epilogue (assembly module-এর stack-frames lesson-এ যার চোখ আমরা করেছি)।
প্রশ্ন: সঠিক, চালানো-যোগ্য x86-64 program লিখতে compiler-কে কোন সিদ্ধান্তগুলো নিতে হয়? সেগুলো হাতে নিয়ে এমন executable বানাব যার exit code আমরা আগেই জানি।
মূল ধারণা
দুইটা মডেল — stack machine বনাম register machine
প্রথম সিদ্ধান্ত: কোন model-এ emit? Stack machine — operand stack-এ, প্রতিটা op দুইটা ঘর pop করে গণনা করে push করে। Register machine — operand named register/memory-তে, instruction-এ স্পষ্ট।
a + b * c :
stack machine register machine (x86-64 style)
push b mov eax, [b]
push c imul eax, [c]
imul add eax, [a]
push a
add
| Stack machine | Register machine | |
|---|---|---|
| কোড ঘনত্ব | খুব ঘন (operand implicit) | তুলনায় বেশি byte |
| Operand delivery | প্রতি op-এ push/pop overhead | register-এ সরাসরি |
| বাস্তব CPU-র সাথে মিল / উদাহরণ | নেই (JVM, CPython, WASM) | এক-এ-এক (x86-64, ARM64) |
Frame layout — প্রতিটা byte-এর ঠিকানা
ফাংশন emit-এর আগে তার activation record — stack frame — এর নকশা। x86-64 stack নিচের দিকে বাড়ে:
│ caller-এর frame
│ ─────────────────────────────
│ outgoing arguments (৭ম+) ← অন্য ফাংশনকে বাড়তি যা পাঠাব
│ ─────────────────────────────
│ return address ← call instruction push করে
│ ─────────────────────────────
│ saved %rbp ← prologue push করে; %rbp এখানে
│ ─────────────────────────────
│ saved callee-saved regs ← %rbx, %r12-15 ব্যবহার করলে
│ ─────────────────────────────
│ locals + spill slots ← [rbp-8], [rbp-16], ...
│ ─────────────────────────────
│ outgoing args area ← call-এর আগে ভরা হয়
▼ %rsp ← ১৬-এ aligned থাকতে হবে `call`-এর আগেচার region: saved regs (callee-saved ছুঁলে পুরনো মান), locals+spills (প্রতিটা value-র ৮-byte ঘর), outgoing args area (৭ম+ argument), alignment (%rsp ১৬-ভাজক বিন্দু থেকে call)।
Prologue আর epilogue
prologue: epilogue:
push %rbp mov %rbp, %rsp (অথবা leave)
mov %rsp, %rbp pop %rbp
sub $48, %rsp ret
push %rbxPrologue: caller-এর frame pointer বাঁচাও, নিজেরটা বসাও (%rsp-র
স্ন্যাপশট %rbp-তে), frame-size বরাদ্দ করো। Epilogue উল্টোটা —
ret return address pop করে সেখানে jump করে।
System V convention আর cmp+jcc
Calling-conventions লেসনের নিয়ম এবার মানতেই হবে — integer/pointer argument প্রথম ৬টা যথাক্রমে %rdi, %rsi, %rdx, %rcx, %r8, %r9, ৭ম+ stack-এ; return value %rax-এ। সংকটের বিষয়: এই ৬টা + %rax/%r10/ %r11 সব caller-saved — call-এর পরে এদের মান অবিশ্বস্ত, তাই generator-কে live value slot-এ রাখতে হয়। Control flow হয় দুই-step:
cmp $40, %rax # rax − 40 → flags (AT&T order!)
jle .Lelse # signed ≤ হলে jump: je/jne/jl/jg/jle/jgeLiteral pool — ধ্রুবক কোথায় থাকে
ছোট immediate ($5) instruction-এর ভেতরেই; string বা ৬৪-bit বড় ধ্রুবক যায় .rodata-তে label দিয়ে — literal pool — access হয় RIP-relative:
.section .rodata
.Lstr0:
.string "sum = %d\n"
.text
lea .Lstr0(%rip), %rdi # ঠিকানা আপেক্ষিক — absolute না
call printfকেন absolute না — PIE/shared library-তে load address বদলায়
(position-independent-code লেসন)। নিয়ম: data-র ঠিকানা সবসময়
symbol(%rip)।
ভেতরে কী ঘটছে
Toy language থেকে .s — তিনটা যান্ত্রিক ধাপ
Instruction selection-এর পরে আমাদের হাতে TAC + প্রতিটা নামের home। Codegen এখন template-বাছাই:
ধাপ ১ — TAC op → template।
TAC: t = x + y homes: x → -8(%rbp), y → -16(%rbp)
mov -8(%rbp), %rax
add -16(%rbp), %rax # immediate হলে: addq $5, %raxDivision বিশেষ: cqto+idivq, quotient %rax-এ। ধাপ ২ — two-operand
constraint: x86-64-এ দুই memory operand অসম্ভব, তাই নিয়ম — প্রথমটা
%rax-এ, দ্বিতীয়টা যেখানে আছে সেখান থেকে; rax/rcx জুটিই যথেষ্ট।
ধাপ ৩ — call discipline।
TAC: t2 = helper(t0, t1)
mov <home of t0>, %rdi
mov <home of t1>, %rsi
call helper
mov %rax, <home of t2>আমাদের সরল নীতি: সব value স্থায়ীভাবে slot-এ, register মুহূর্তের ধার — slow কিন্তু নিরাপদ, call-এর আগে spill-ই নেই।
- TAC: t2 = a + bIR — ভার্চুয়াল, কোনো ISA-র ভাষা না
- Regalloc: a→[rbp-8], b→[rbp-16], t2→raxপ্রতিটা নামের concrete home
- Template: mov+add pairISA constraint মেনে pattern
- .s text → assembler → bytesপরের লেসনের গল্প
উদাহরণ
সম্পূর্ণ উদাহরণ — toy program → .s → চলমান process
int a = 6;
int b = 7;
int c = a * b + 2; // 44
if (c > 40)
c = c - 4; // 40
return c; // main-এর return = exit codeCompiler-এর বাকি অংশ দিয়েছে TAC আর homes (a→-8, b→-16, c→-24, temps → -32..-64); generated .s:
.text
.globl main
main:
push %rbp
mov %rsp, %rbp
sub $64, %rsp # slots + ১৬-এ alignment
movq $6, %rax
movq %rax, -8(%rbp) # a = 6
movq $7, %rax
movq %rax, -16(%rbp) # b = 7
movq -8(%rbp), %rax # t2 = a * b
imulq -16(%rbp), %rax
addq $2, %rax # t3 = t2 + 2
movq %rax, -24(%rbp) # c = t3
cmpq $40, -24(%rbp) # c − 40 → flags
jle .Lelse # c ≤ 40 হলে skip
movq -24(%rbp), %rax # t4 = c - 4
subq $4, %rax
movq %rax, -24(%rbp)
.Lelse:
movq -24(%rbp), %rax # ret c → convention অনুযায়ী %rax
mov %rbp, %rsp
pop %rbp
ret$ gcc gen.s -o gen # assemble + link এক ধাপে
$ ./gen # main-এর return value = exit code
$ echo $?
40echo $? = 40 — হিসাবের সাথে হুবহু। Codegen-যাচাইয়ের সস্তা oracle:
ফলাফল exit code বানাও, shell-ই verifier।
ছবিতে
call-এর মুহূর্তে দুই frame-এর সীমানা
যদি উপরের প্রোগ্রাম c-কে twice(int x)-এ পাঠাত, call twice
execute-এর ঠিক পরে স্ট্যাক:
উচ্চ ঠিকানা
┌────────────────────────────────┐
│ main-এর frame (অক্ষত): │
│ [rbp-8] a=6, [rbp-16] b=7, │
│ [rbp-24] c=44 │
│ outgoing args area │ ◄─ ৭ম+ arg এখানে থাকত
├────────────────────────────────┤
│ return address → main-এ ফেরার লাইন │ ← call twice push করেছে
├────────────────────────────────┤ ◄─ বর্তমান %rsp
│ twice-এর saved %rbp │ ← twice-এর prologue চলছে
├────────────────────────────────┤
│ twice-এর locals... │
└────────────────────────────────┘
নিম্ন ঠিকানালক্ষণ: call নিজেই return address push করে — emit লাগে না; callee-র
prologue-ই তার frame বানায় — caller শুধু argument বসায়; saved-%rbp
chain-ই debugger-এর backtrace-এর হাঁটা-পথ।
নিজে চালিয়ে দেখুন
নিজের হাতে .s emit করুন — template থেকে
Toy program: a=12, b=5, s=a*b; if (s>=50) s=s+1; return s — আগে
কাগজে exit code হিসাব করুন। Template:
.text
.globl main
main:
push %rbp
mov %rsp, %rbp
sub $16, %rsp
movq $12, %rax
movq %rax, -8(%rbp) # a = 12
movq $5, %rax
movq %rax, -16(%rbp) # b = 5
movq -8(%rbp), %rax # s = a * b
??? -16(%rbp), %rax # কোন instruction?
movq %rax, -24(%rbp)
cmpq $50, -24(%rbp)
??? .Lskip # s ≥ 50 না হলে skip — কোন jcc?
movq -24(%rbp), %rax
addq $1, %rax
movq %rax, -24(%rbp)
.Lskip:
movq -24(%rbp), %rax # return s
mov %rbp, %rsp
pop %rbp
retgcc hand.s -o hand && ./hand
echo $? # হিসাবের সাথে মিলল?এবার পেশাদার shape — clang -O0 -S probe.c: একই হাড্ডি খুঁজুন —
prologue জোড়া, [rbp-N], cmp+jcc, %rax-এ return। পার্থক্য শৈলীর:
clang-এর directive, movl, গুটানো instruction। Codegen-এ শৈলীর
জায়গা নেই, সঠিকতাই সব。
Template-এর খালি জায়গা শুধু নিয়ম মেনে পূরণ করেই চলমান native program বানানো যায়, আর shell-এর exit code দিয়ে machine-precision-এ যাচাই হয়।
নিজে বানান
CodeGen — TAC থেকে x86-64 .s emit করার class
- TAC line parser — label, assignment, binop, branch, call, ret শনাক্ত করবে
- প্রতিটা variable/temp-এর home টেবিল (slot offset)
- Prologue/epilogue emitter — frame size ১৬-এ align করে
- প্রতিটা op-এর template — binop rax/rcx, cmp+jcc জোড়া, System V arg register
- Output .s gcc দিয়ে assemble-run করে exit code যাচাই
Generator-টা এই লেসনের প্রতিটা নিয়ম প্রয়োগ করে:
import sys
ARG_REGS = ['%rdi', '%rsi', '%rdx', '%rcx', '%r8', '%r9']
BINOP = {'+': 'addq', '-': 'subq', '*': 'imulq'}
class CodeGen:
def __init__(self, homes):
self.homes = homes # {'a': '-8', ...} (rbp-relative)
self.slots = len(homes) # সব value slot-এই থাকে
self.out = []
self.label_n = 0
def home(self, v):
if v.startswith('%'): # ইতিমধ্যে register
return v
return f"{self.homes[v]}(%rbp)"
def get(self, v, reg='%rax'):
"""v-র মান reg-এ আনো"""
self.emit(f"movq {self.home(v)}, {reg}")
def put(self, v, reg='%rax'):
"""reg-এর মান v-তে জমা"""
self.emit(f"movq {reg}, {self.home(v)}")
def emit(self, line):
self.out.append(f" {line}")
def label(self, text=None):
name = text or f".L{self.label_n}"
self.label_n += 1
self.out.append(f"{name}:")
return name
def prologue(self):
frame = ((self.slots * 8 + 15) // 16) * 16 # byte-এ align
self.out += [f" .text", f" .globl main", f"main:",
f" push %rbp", f" mov %rsp, %rbp",
f" sub ${frame}, %rsp"]
def epilogue(self):
self.out += [f" mov %rbp, %rsp", f" pop %rbp", f" ret"]
def gen_binop(self, dest, a, op, b):
self.get(a, '%rax')
if op == '/':
self.emit("cqto")
self.emit(f"idivq {self.home(b)}")
else:
rhs = f"${b}" if b.lstrip('-').isdigit() else self.home(b)
self.emit(f"{BINOP[op]} {rhs}, %rax")
self.put(dest)
def gen_call(self, dest, fname, args):
for i, a in enumerate(args[:len(ARG_REGS)]):
self.get(a, ARG_REGS[i])
self.emit(f"call {fname}")
if dest:
self.put(dest)
def generate(self, tac_lines):
self.prologue()
for line in tac_lines:
line = line.strip()
if not line:
continue
if line.endswith(':'):
self.label(line.rstrip(':') or None)
elif line.startswith('goto'):
self.emit(f"jmp {line.split()[1]}")
elif line.startswith('if'):
# রূপ: if x OP y goto L
_, x, op, y, _, lbl = line.split()
self.get(x, '%rax')
rhs = f"${y}" if y.isdigit() else self.home(y)
self.emit(f"cmpq {rhs}, %rax")
mnem = {'<': 'jl', '>': 'jg', '<=': 'jle',
'>=': 'jge', '==': 'je', '!=': 'jne'}[op]
self.emit(mnem + ' ' + lbl)
elif line.startswith('ret'):
self.get(line.split()[1], '%rax')
self.epilogue()
elif '=' in line:
dest, expr = [s.strip() for s in line.split('=', 1)]
parts = expr.split()
if len(parts) == 1: # copy: d = x
src = parts[0]
if src.lstrip('-').isdigit():
self.emit(f"movq ${src}, %rax")
else:
self.get(src)
self.put(dest)
elif parts[1] == 'CALL':
self.gen_call(dest, parts[2], parts[3:])
else:
self.gen_binop(dest, parts[0], parts[1], parts[2])
return "\n".join(self.out) + "\n"
tac = """
t0 = 6
a = t0
t1 = 7
b = t1
t2 = a * b
t3 = t2 + 2
c = t3
if c <= 40 goto L_else
t4 = c - 4
c = t4
L_else:
ret c
"""
homes = {'t0': '-32', 't1': '-40', 't2': '-48',
't3': '-56', 't4': '-64',
'a': '-8', 'b': '-16', 'c': '-24'}
print(CodeGen(homes).generate(tac.splitlines()))চালান ও যাচাই করুন:
python3 codegen.py > gen.s
gcc gen.s -o gen && ./gen; echo "exit=$?"প্রত্যাশিত output:
exit=40(macOS-এ _main rename করতে ভুলবেন না।) উপরের example section-এর
hand-written .s-এর সাথে generator-ের output পাশাপাশি রাখুন —
একই logic-এর দুই উপস্থাপনা।
নিজে বাড়ান:
- Division (
cqto/idivqtemplate তৈরিই আছে) —17 / 5emit করে exit code 3 যাচাই করুন printbuiltin — .rodata literal pool + printf call (variadic-এ%al = 0কেন দরকার, ভাবুন)- “সব value slot-এ” নীতি ভেঙে hot value register-এ আনুন — call-boundary spill ছাড়া কোথায় ভাঙে, test লিখুন
বাস্তব সিস্টেমে
Code generation যেখানে বাস্তবে হয়
LLVM backend-সমূহ। আজকের কাজের industrial রূপ — instruction selection, regalloc, MC encoding; Lattner-Adve-র virtual ISA চুক্তিতে Rust, Swift, Zig, Julia frontend-backend বিভাজনের সুবিধা নিচ্ছে।
GCC machine description আর Cranelift। GCC-র প্রতিটা architecture- এর .md file-এ আমাদের template-দের মতোই pattern; Cranelift (Rust wasm/ debug) দেখায় quality আর compile-speed দুইটা আলাদা dial।
Debug build থেকে console SDK। -O0 ঠিক আমাদের নীতিই — সব value
slot-এ, ধীর কিন্তু debugger-এর জন্য নিখুঁত; avr-gcc থেকে console
backend — একই prologue/epilogue discipline, ভিন্ন register আর ABI।
যে ভুলগুলো সবাই করে
“Code generation-ই সবচেয়ে গুরুত্বপূর্ণ ধাপ — এখান থেকেই optimized code আসে।”
উল্টো। বেশিরভাগ “বুদ্ধি” আগের ধাপে — IR optimization আর scheduling-এ। Codegen যান্ত্রিক: সঠিক template, মানা constraint; naive codegen মন্দ কোড দিলেও ভুল না — সঠিকতাই প্রথম চুক্তি। Target-specific tuning আছে, কিন্তু secondary।
“Stack machine model পুরনো — সবাই register machine-এ গেছে।”
Native codegen-এ ঠিকই; কিন্তু JVM, CPython, WASM, CIL সবই stack-based — portability, ছোট size, সহজ verification (WASM-এ প্রায় free)। এরা interpreter/JIT-এর উপর চলে — jit-compilation লেসনে সেই সেতু。
“প্রতিটা ফাংশনেই push %rbp prologue লাগবেই।”
Convention, বাধ্যবাধকতা না। Leaf function প্রায়ই frame pointer-ই
রাখে না — সব ঠিকানা %rsp থেকে, -O2-এর ডিফল্ট। Trade-off: এক register
খালি, কিন্তু chain ভাঙে — unwind table ছাড়া profiler stack পড়তে পারে না।
“AT&T আর Intel syntax দুইটা আলাদা instruction set।”
লেখার রীতি, semantics এক: addq -16(%rbp), %rax = add rax, [rbp-16]
— same bytes। পার্থক্য operand order, size suffix, % prefix;
.intel_syntax বা objdump -Mintel দিয়ে রূপ বদলায়।
বুঝেছেন কি না দেখুন
1System V AMD64-এ প্রথম ছয়টা integer argument কোন register-এ যায়,
return value কোথায় ফেরে? ৭ম argument কোথায়?
স্মরণ
ক্রমে %rdi, %rsi, %rdx, %rcx, %r8, %r9; return %rax-এ। ৭ম+ caller-এর outgoing args area-য় (stack)। সূত্র: “di, si, dx, cx, 8, 9”।
2৩টা local + ২টা spill slot (৮ byte করে) আর একটা push %rbx —
prologue-এর sub $N, %rsp-এ N কত, ১৬-byte alignment মেনে?
প্রয়োগ
push %rbx —
prologue-এর sub $N, %rsp-এ N কত, ১৬-byte alignment মেনে?Slot ৫×৮ = ৪০; %rbx push = ৮; মোট ৪৮ — ইতিমধ্যে ১৬-এ ভাগ যায়, N=48। মনে রাখুন push-ও %rsp নাড়ায়, alignment হিসাবে গোনতে হয়।
3cmpq $40, -24(%rbp) এর পরে jle .Lelse কেন, jge কেন না? AT&T
operand-order ভুল হলে কী ধরনের bug হবে?
যুক্তি
cmpq $40, -24(%rbp) এর পরে jle .Lelse কেন, jge কেন না? AT&T
operand-order ভুল হলে কী ধরনের bug হবে?AT&T-তে cmp a,b গণনা b−a = c−40; branch শর্ত “if c>40 body” —
উল্টো পথে যাওয়ার শর্ত c≤40 → jle। Order ভুল হলে condition পুরো
উল্টে যায় — program crash করে না, নীরবে ভুল উত্তর দেয়; এটাই
logic-inversion bug-এর বিপদ।
4String literal pool .rodata-তে সরালে কোন নতুন বাগ সম্ভব, কীভাবে
যাচাইবেন?
ডিজাইন
ঝুঁকি: ভুল label index, .rodata writable segment-এ মিশে যাওয়া,
displacement range ছাড়ানো। যাচাই: distinct-content test, readelf -x .rodata-তে প্রকৃত byte, readelf -l-এ segment permission-এ W না থাকা。
এরপর কী
এরপর কী
Compiler এখন চলে এমন .s emit করে — কিন্তু assemble-link gcc-র কাঁধে। ওই কমান্ডের ভেতরে assembler .s→.o, তারপর linker crt startup আর libc জুড়ে executable বানায়। Relocation কীভাবে ভাঙা address জোড়ে, resolution কীভাবে “undefined reference”/“multiple definition”-এর জন্ম দেয় — পরের লেসনে mini-linker লিখে দেখব।
আরও পড়ুন
- Compilers: Principles, Techniques, and Tools ("Dragon Book"), Code Generation অধ্যায় — Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman · Code generation-এর ক্লাসিক প্রামাণ্য আলোচনা — target program, মেমরি ম্যাপিং আর instruction selection-এর ভিত্তি
- System V Application Binary Interface — AMD64 Architecture Processor Supplement · আর্গুমেন্ট register, stack alignment আর frame layout-এর সরকারি স্পেসিফিকেশন — এই লেসনের calling convention অংশের উৎস
- "LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation" — Chris Lattner, Vikram Adve · ভার্চুয়াল instruction set কীভাবে frontend আর backend আলাদা করে — code generation কেন আলাদা স্তর হওয়া উচিত তার সবচেয়ে প্রভাবশালী যুক্তি