Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ১৩/১৬অ্যাডভান্সড১ ঘণ্টা

Code Generation — শেষ ধাপে সিলিকনের ভাষায় অনুবাদ

Code Generation

Register allocation শেষ হলে code generator টার্গেট ISA-র প্রকৃত instruction লেখে — frame layout ঠিক করে, prologue/epilogue emit করে, calling convention মেনে — আর এই লেসনে আমরা toy compiler-এর পুরো ধাপটা হাতে করে করে দেখি।

এই লেসন শেষে আপনি পারবেন

  • Stack-machine আর register-machine code generation-এর মৌলিক পার্থক্য ব্যাখ্যা করে একই expression-এর জন্য দুই রকম emitted code পাশাপাশি তুলনা করতে পারবেন
  • একটা ফাংশনের prologue আর epilogue emit করতে পারবেন এবং frame layout-এর প্রতিটা region — saved registers, locals, spill slot, outgoing argument area — ঠিক কোন byte-offset-এ বসে তা নিজে হিসাব করতে পারবেন
  • Toy language-এর TAC-কে x86-64-এ অনুবাদ করতে পারবেন — int variable [rbp-x] slot-এ, binary operation rax/rcx জুটি দিয়ে, control flow cmp+jcc দিয়ে, function call System V convention মেনে emit করতে পারবেন
  • নিজের generate করা .s ফাইল gcc দিয়ে assemble-link-run করে exit code দেখে সঠিকতা যাচাই করতে পারবেন, আর clang/gcc-এর নিজস্ব আউটপুটের shape-এর সাথে তুলনা করতে পারবেন
  • String বা বড় ধ্রুবক literal pool/.rodata section-এ label দিয়ে রেখে RIP-relative ভাবে access করার pattern ব্যাখ্যা করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

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 machineRegister machine
কোড ঘনত্বখুব ঘন (operand implicit)তুলনায় বেশি byte
Operand deliveryপ্রতি op-এ push/pop overheadregister-এ সরাসরি
বাস্তব 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`-এর আগে
Frame layout — উঁচু ঠিকানা থেকে নিচে: outgoing args, return address, saved rbp, saved regs, locals/spills, outgoing area।

চার 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 %rbx

Prologue: 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/jge

Literal 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, %rax

Division বিশেষ: 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 line কোথা থেকে কোথায় যায়
  1. TAC: t2 = a + bIR — ভার্চুয়াল, কোনো ISA-র ভাষা না
  2. Regalloc: a→[rbp-8], b→[rbp-16], t2→raxপ্রতিটা নামের concrete home
  3. Template: mov+add pairISA constraint মেনে pattern
  4. .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 code

Compiler-এর বাকি অংশ দিয়েছে 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 $?
40

echo $? = 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-এর হাঁটা-পথ।

নিজে চালিয়ে দেখুন

EXPERIMENT

নিজের হাতে .s emit করুন — template থেকে

Linux বা macOS (gcc/clang, x86-64; macOS-এ main → _main)· ১৫ মিনিট

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
    ret
gcc 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-এ যাচাই হয়।

নিজে বানান

BUILD IT

CodeGen — TAC থেকে x86-64 .s emit করার class

Python · ●●●○○
  1. TAC line parser — label, assignment, binop, branch, call, ret শনাক্ত করবে
  2. প্রতিটা variable/temp-এর home টেবিল (slot offset)
  3. Prologue/epilogue emitter — frame size ১৬-এ align করে
  4. প্রতিটা op-এর template — binop rax/rcx, cmp+jcc জোড়া, System V arg register
  5. 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-এর দুই উপস্থাপনা।

নিজে বাড়ান:

  1. Division (cqto/idivq template তৈরিই আছে) — 17 / 5 emit করে exit code 3 যাচাই করুন
  2. print builtin — .rodata literal pool + printf call (variadic-এ %al = 0 কেন দরকার, ভাবুন)
  3. “সব 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 দিয়ে রূপ বদলায়।

বুঝেছেন কি না দেখুন

1

System 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 মেনে?

প্রয়োগ

Slot ৫×৮ = ৪০; %rbx push = ৮; মোট ৪৮ — ইতিমধ্যে ১৬-এ ভাগ যায়, N=48। মনে রাখুন push-ও %rsp নাড়ায়, alignment হিসাবে গোনতে হয়।

3

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-এর বিপদ।

4

String 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 কেন আলাদা স্তর হওয়া উচিত তার সবচেয়ে প্রভাবশালী যুক্তি