Compilers — Build a Small Language from Start to Finish
Count and Read the Output of gcc and clang
Goal
You build readasm.py, a tool that cuts gcc's -S assembly and clang's LLVM IR by function and counts them, and use it to confirm in numbers the differences between -O0 and -O2 (-O1) — inlining, constant folding, turning division into multiplication, turning tail recursion into a loop, and mem2reg and φ.
Why it matters
If you do not know what optimization did, you only guess when facing a performance problem, and you end up suspecting the compiler when facing a bug that appears only at -O2. If you read the output directly, you can check both. The grader compiles anew at grading time variant C files with the constants and divisors changed, so your tool must read according to the rules to pass.
The rules
어셈블리(gcc -S) 맨 앞이 글자·_·. 인 'name:' 줄이 이름표. '.' 으로 시작하면 안쪽 이름표(.L3 등),
아니면 함수의 시작. 들여 쓴 줄 가운데 '.' 으로 시작하지 않는 것이 명령. # 뒤는 주석.
명령은 앞뒤 공백을 걷고 명령어와 피연산자 사이 공백을 하나로('movl\t$1, %eax' → 'movl $1, %eax')
functions(asm) {함수: [명령…]} (이름표·지시어 제외) · count(asm, f) 는 그 수(endbr64 포함)
calls(asm, f) call·callq 의 대상(나온 순서), '@PLT' 는 뗀다
returns_constant endbr64 를 뺀 몸체가 [mov(l|q) $값, %eax|%rax ; ret] 이면 그 값, [xor %eax, %eax ; ret] 이면 0, 아니면 None
div_magic(asm, f) f 안 첫 movabs(q) 의 즉시값을 부호 없는 64비트로 본 '0x' + 16자리 소문자 16진수, 없으면 None
back_edges(asm, f) f 안에서 앞서 나온 안쪽 이름표로 가는 j* 명령의 수
IR(clang -emit-llvm) 'define … @이름(' 부터 맨 앞의 '}' 까지. 이름표 줄('3:')·빈 줄 제외, ';' 뒤는 주석
opcode(명령) '%5 = add nsw i64 …' → add · 'store …' → store · tail/musttail/notail 은 건너뛴다
opcode_counts {opcode: 수} · ssa_summary(O0 IR, O1 IR, f) → {"O0": {alloca, load, store, phi}, "O1": {…}}
Steps
- Fill in
asm_lines(asm)andfunctions(asm)in/root/mini/readasm.py. Make the material first:gcc -O0 -S -fno-asynchronous-unwind-tables /opt/fixtures/mini/real/opt.c -o opt-O0.s(and likewise for -O2). - Fill in
mnemonic,count, andcalls— you can see whether thesquarecall disappears at -O2. - Fill in
returns_constant— whatsum_to,always, andneverfold into at -O2. - Fill in
div_magic— the grader also looks at variants with the divisor changed. - Fill in
back_edges—factis a call at -O0 and a loop at -O2. - Fill in
ir_functions,opcode, andopcode_counts(clang -O0 -S -emit-llvm …, and -O1 too). - Fill in
ssa_summary. - Write the values read with your readasm into
/root/mini/real_report.json:fib_O0andfib_O2(the instruction counts of fib in opt.c),fib_mini(the instruction count ofmini_f_fibin the assembly of/opt/fixtures/mini/programs/fib.minibuilt with the module 8 code generator — viapython3 mini.py asm),sum_to_O2,div10_magic,fact_O2_calls(the number of calls),fact_O2_back_edges, andfib_O1_phi(the number of phi in fib in the -O1 IR).
Notes
- The files up to the code generator (
codegen.py,mini.py) are laid out when you start the lab. - With
objdump -d --no-show-raw-insn opt.o, you can see the machine code after baking in the same shape (this lab's grading is done with the -S output). - Common mistakes: counting labels (.L3:) as instructions, leaving tabs as they are so the text differs, writing the magic number as a signed number, counting every jump as a loop, and counting the label lines of IR as instructions.
- The reason the file is named readasm: inspect is the name of a Python standard module, so saving under that name would hide the standard module.
- The session starts at 60 minutes and you can extend it with the +time button, and when it ends
/root/minidisappears.
Cut the assembly into functions
On each line, strip what follows #, and treat a 'name:' that starts at the very beginning as a label. A label starting with '.' is an inner label of the current function, and otherwise it is the start of a new function. Among indented lines, only those that do not start with '.' are instructions, and reduce whitespace to one with split(None, 1).
Instruction count and calls
count is the length of the functions result (endbr64 is also an instruction). calls collects the targets of instructions whose first word is call or callq, stripping what follows '@'. Compare the call list of sum_squares at -O0 and -O2.
A function folded to a single constant
Look only when the body without endbr64 is exactly two instructions and the second is ret. If the first instruction is mov(l|q) $value, %eax (or %rax), it is that value, and if it clears %eax against itself with xor, it is 0.
Multiplication instead of division — the magic number
Look through the function's instructions from the front for movabs (or movabsq) $number, %register. gcc writes it as a signed decimal, so take the remainder mod 2 ** 64 to make it unsigned and write it as '0x%016x'.
Did the recursion become a loop
Looking at the lines of one function in order with asm_lines, collect the inner labels that have appeared so far. If the target of an instruction starting with j is an already seen label, it is a backward jump. In the -O2 fact, the call disappears and this count appears.
Cut LLVM IR into functions
Open a function at a line 'define … @name(' and close it at a line that begins with '}'. For the lines in between, strip what follows ';', and drop those that are empty or are a label like '3:'. For opcode, if there is an '=', the first word after it; skip tail, musttail, and notail.
Count stack slots turning into φ
Get opcode_counts from each of the two IRs and pull out only four — alloca, load, store, and phi (0 if absent). The alloca at -O0 becoming 0 at -O1 and phi appearing is the real thing behind the φ positions you computed in module 7.
gcc and your compiler side by side
Make the material again (gcc -O0/-O2 -S, clang -O1 -emit-llvm, mini.py asm), compute the eight values with your readasm functions, and write them. Do not copy the numbers by hand — the grader rereads with the same compiler and compares.