Full beginner lecture
Toolchain Pseudoinstructions
Opening Intuition
Toolchain과 Pseudoinstructions를 처음 보면 많은 학생이 “그냥 assembler가 조금 친절하게 해 주는 것” 정도로 생각합니다. 그 말이 완전히 틀린 것은 아니지만, 시험에서는 그 정도 이해로는 부족합니다. Rechnerorganisation에서 이 주제의 핵심은 “내가 쓴 한 줄의 assembly가 실제 CPU가 읽는 한 줄과 항상 같지는 않다”는 사실을 정확히 구분하는 것입니다. RISC-V 코드를 눈으로 볼 때는 li, mv, nop, ret, j, call, la 같은 명령이 자연스럽게 보입니다. 그런데 CPU의 instruction memory에 들어가는 machine code는 이런 편의 명령을 그대로 모르는 경우가 많습니다. CPU가 아는 것은 opcode, funct3, funct7, rd, rs1, rs2, immediate 같은 field로 encoding된 real instruction입니다.
초보자에게 가장 좋은 비유는 번역과 포장입니다. C program은 사람이 편하게 쓰는 높은 수준의 설명입니다. Compiler는 그것을 RISC-V assembly라는 더 낮은 수준의 설명으로 번역합니다. Assembler는 assembly text를 object file 안의 machine code와 symbol 정보로 바꿉니다. Linker는 여러 object file과 library를 합치고, 아직 확정되지 않은 주소를 고쳐서 executable을 만듭니다. Loader나 Betriebssystem은 그 executable을 memory에 올리고 PC를 entry point로 맞춥니다. 이 전체 흐름을 Toolchain이라고 부릅니다.
Pseudoinstruction은 이 흐름 중 특히 assembler 앞에서 나타나는 “사람에게 친절한 assembly 문법”입니다. 예를 들어 mv t0, t1은 CPU에게 “move라는 별도 연산을 해라”라고 말하는 것이 아닙니다. Assembler가 이것을 보통 addi t0, t1, 0으로 바꿉니다. ret도 CPU가 “return”이라는 고급 개념을 직접 실행하는 것이 아니라 jalr x0, ra, 0 같은 real instruction으로 바뀝니다. li는 더 조심해야 합니다. 작은 상수라면 addi rd, x0, imm 하나로 충분하지만, 큰 상수라면 lui와 addi 또는 다른 조합이 필요합니다. 즉 Pseudoinstruction은 한 줄이 한 줄로 바뀔 수도 있고, 한 줄이 여러 줄로 바뀔 수도 있습니다.
이 주제가 시험에서 중요한 이유는 단순 암기 때문이 아닙니다. Pseudoinstruction을 모르면 code trace에서 PC 증가를 잘못 셉니다. 한 줄짜리 li가 실제로 두 instruction이면 PC는 4가 아니라 8만큼 진행됩니다. call이 실제로 auipc plus jalr로 바뀌면 ra에 저장되는 return address도 달라집니다. bgt 같은 pseudo branch가 실제로 operand 순서를 바꾼 blt로 바뀌면 조건 해석을 반대로 착각하기 쉽습니다. 그리고 compiler, assembler, linker의 역할을 모르면 “label 주소는 언제 결정되는가?”, “external symbol은 누가 해결하는가?”, “object file과 executable은 무엇이 다른가?” 같은 Theoriefragen에서 점수를 잃습니다.
오늘의 목표는 한 문장으로 정리됩니다. Assembly source에 보이는 명령을 그대로 믿지 말고, “이 줄은 real instruction인가, Pseudobefehl인가, assembler가 어떤 base instruction으로 expansion하는가, 주소와 symbol은 어느 단계에서 확정되는가”를 매번 따져 볼 수 있어야 합니다.
What You Must Know Before This
먼저 RISC-V register 이름을 알아야 합니다. RISC-V에는 x0부터 x31까지 32개의 integer register가 있고, ABI alias로 zero, ra, sp, gp, tp, t0, t1, s0, a0 같은 이름을 씁니다. x0 또는 zero는 항상 0입니다. 여기에 값을 써도 저장되지 않습니다. ra는 return address register입니다. sp는 stack pointer입니다. a0부터 a7은 function argument와 return value에 자주 쓰이고, t0부터 t6은 temporary register입니다. Pseudoinstruction expansion을 볼 때 zero와 ra가 특히 자주 등장합니다.
둘째, immediate의 크기 제한을 알아야 합니다. addi의 immediate는 12-bit signed immediate입니다. 즉 대략 -2048부터 2047까지 표현할 수 있습니다. 그래서 li t0, 7은 addi t0, zero, 7로 충분하지만, li t0, 0x12345678은 addi 하나로 불가능합니다. 이런 경우 assembler는 lui로 상위 bit를 넣고, addi로 하위 bit를 보정하는 식의 여러 real instruction을 만듭니다.
셋째, PC가 instruction 단위가 아니라 byte address 기준으로 움직인다는 점이 필요합니다. 보통 RV32I의 기본 instruction은 4 byte입니다. compressed instruction을 고려하지 않는 범위에서는 한 real instruction 실행 후 PC가 보통 PC + 4가 됩니다. 그러나 “source line 한 줄”이 아니라 “실제 real instruction 한 개”가 4 byte입니다. 따라서 pseudo 한 줄이 두 real instruction이면 PC는 그 source line을 지나며 총 8 byte 움직일 수 있습니다.
넷째, label과 symbol을 주소의 이름표로 이해해야 합니다. loop:, main:, msg: 같은 label은 사람이 읽기 좋은 이름입니다. Assembler와 linker는 이 이름을 실제 address 또는 relocation 대상과 연결합니다. 같은 assembly file 안에서 가까운 branch target은 assembler가 바로 offset을 계산할 수 있습니다. 반면 다른 object file에 있는 함수나 data symbol은 linker가 나중에 전체 배치를 본 뒤 해결합니다.
다섯째, real instruction과 pseudo instruction을 구분해야 합니다. addi, add, sub, lw, sw, beq, jal, jalr, lui, auipc는 RV32I의 real instruction입니다. mul은 RV32I 기본은 아니지만 M-Erweiterung이 있으면 real instruction입니다. 반대로 mv, li, nop, not, neg, seqz, snez, beqz, bnez, bgt, j, jr, ret, call, tail, la 등은 assembler Pseudoinstructions로 다루는 것이 보통입니다. 시험에서 ISA subset이 주어지면 그 subset 기준으로 판단해야 합니다.
Why This Exists In Rechnerorganisation
Rechnerorganisation은 단순히 assembly 문법을 배우는 과목이 아닙니다. 이 과목은 software가 hardware에서 어떻게 실행 가능한 형태가 되는지, 그리고 그 형태가 Datenpfad, Steuerwerk, memory, pipeline으로 어떻게 흘러가는지 묻습니다. Toolchain과 Pseudoinstructions는 그 연결부입니다. 사람이 쓴 C code와 CPU가 읽는 machine code 사이에 있는 층을 이해하지 못하면 이후의 datapath와 pipeline도 흐릿해집니다.
예를 들어 ret을 봅시다. 사람이 보기에는 “함수에서 돌아간다”입니다. 하지만 single-cycle Datenpfad 관점에서는 ret이라는 control signal이 따로 없습니다. 실제로는 jalr x0, ra, 0입니다. 그러면 ALU는 ra + 0을 계산하고, PC source mux는 그 결과를 선택하며, rd가 x0이기 때문에 link 값은 버려집니다. 이렇게 pseudo를 real instruction으로 바꾸면 갑자기 datapath 질문으로 연결됩니다.
또 la a0, msg를 봅시다. 사람이 보기에는 “label의 주소를 a0에 넣는다”입니다. 하지만 executable에서는 msg가 memory의 어느 address에 배치되는지가 중요합니다. 작은 범위라면 PC-relative 계산이 가능하고, 보통 auipc와 addi 조합이 나옵니다. 이때 assembler는 relocation record를 남기고, linker가 최종 주소를 알고 나서 immediate field를 고칩니다. 그러면 이 주제는 assembler/linker, object file, relocation까지 연결됩니다.
Übung 4 유형의 문제에서는 Pseudobefehle, Compiler/Assembler/Linker, M extension, shifts가 함께 등장합니다. 이는 우연이 아닙니다. Pseudoinstruction은 “보이는 assembly와 실제 encoding의 차이”를 훈련시키고, toolchain은 “어느 tool이 어느 변환을 담당하는가”를 훈련시킵니다. 둘 다 시험장에서 trace와 encoding 문제를 풀 때 실수 방지 장치가 됩니다.
Key Terms, Slowly
1. Toolchain
Toolchain은 Quellcode에서 executable까지 이어지는 도구들의 연쇄입니다. 전형적인 흐름은 C source -> Compiler -> assembly -> Assembler -> object file -> Linker -> executable입니다. 각 단계가 입력과 출력을 바꿉니다. 시험 답안에서는 “compiler는 high-level language를 assembly 또는 intermediate/machine-oriented code로 변환한다”, “assembler는 assembly를 object code로 변환하고 pseudo를 real instruction으로 확장한다”, “linker는 여러 object file을 합치고 symbol reference를 해결한다”처럼 역할을 분리해야 합니다.
2. Quellcode
Quellcode는 사람이 작성한 source code입니다. C, C++, Rust 같은 언어일 수 있습니다. RO에서는 C code가 RISC-V assembly로 어떻게 내려가는지가 중요합니다. CPU는 C 문법을 직접 실행하지 않습니다. CPU는 memory에서 instruction bits를 fetch하고 decode할 뿐입니다.
3. Compiler
Compiler는 high-level program을 target architecture에 맞는 low-level representation으로 바꿉니다. 여기서는 C를 RISC-V assembly로 내리는 도구라고 생각하면 됩니다. Compiler는 최적화, register allocation, calling convention 적용, stack frame 구성 같은 일을 합니다. 그러나 external function의 최종 address를 혼자 모두 알지는 못합니다.
4. Assembler
Assembler는 assembly text를 machine code가 들어 있는 object file로 바꿉니다. 여기서 Pseudoinstruction expansion이 일어납니다. mv를 addi로, nop을 addi x0, x0, 0으로, ret을 jalr x0, ra, 0으로 바꾸는 식입니다. 또한 label을 가능한 범위에서 offset으로 바꾸고, 아직 확정되지 않은 symbol에 대해서는 relocation 정보를 남깁니다.
5. Linker
Linker는 여러 object file과 library를 합쳐 executable을 만듭니다. main.o가 foo를 호출하지만 foo가 foo.o에 있다면, assembler 단계에서는 foo의 최종 주소를 모를 수 있습니다. Linker가 모든 object를 배치하고 symbol table을 보고 reference를 해결합니다. 그래서 “external symbol address는 linker가 해결한다”는 문장이 중요합니다.
6. Object file
Object file은 assembler의 출력입니다. 보통 .o 확장자를 생각하면 됩니다. 이미 machine code 조각을 포함하지만, 완전한 실행 파일은 아닐 수 있습니다. section, symbol table, relocation entry 같은 정보가 들어 있습니다. 즉 object file은 “거의 기계어지만 아직 연결과 주소 보정이 필요할 수 있는 중간 결과”입니다.
7. Executable
Executable은 linker의 출력입니다. 예를 들어 ELF file이 될 수 있습니다. 실행 가능한 code section, data section, entry point, 필요한 metadata를 포함합니다. Betriebssystem loader가 이 파일을 memory에 올리고 실행을 시작할 수 있습니다.
8. Pseudoinstruction / Pseudobefehl
Pseudoinstruction은 CPU ISA의 실제 encoding 명령이 아니라 assembler가 제공하는 편의 문법입니다. 하나의 pseudo가 하나의 real instruction으로 바뀔 수도 있고, 여러 real instruction으로 바뀔 수도 있습니다. 시험에서는 “이 명령이 CPU가 직접 decode하는 instruction인가?”를 질문해야 합니다.
9. Real instruction / Basisinstruktion
Real instruction은 ISA에 정의되어 있고 machine code로 encoding되는 명령입니다. RV32I 기준으로 add, sub, addi, lw, sw, beq, jal, jalr, lui, auipc 등이 있습니다. M-Erweiterung을 포함하면 mul, div, rem 같은 명령도 real instruction입니다.
10. Expansion
Expansion은 pseudo를 real instruction sequence로 바꾸는 과정입니다. 예를 들어 beqz t0, done은 beq t0, zero, done으로 확장됩니다. bgt t0, t1, L은 blt t1, t0, L처럼 operand 순서가 바뀌는 real branch로 확장됩니다.
11. Relocation
Relocation은 “주소가 아직 최종 확정되지 않았으니 linker가 나중에 고쳐야 한다”는 정보입니다. call external_function이나 la a0, global_data는 최종 배치에 따라 immediate가 달라질 수 있습니다. Assembler는 임시 값을 넣고 relocation entry를 남길 수 있으며, linker가 나중에 정확한 값을 채웁니다.
12. Symbol table
Symbol table은 이름과 위치 정보를 담는 표입니다. main, printf, array, loop 같은 symbol이 어느 section의 어느 offset에 있는지 기록됩니다. Linker는 symbol table을 이용해 서로 다른 object file 사이의 reference를 연결합니다.
13. ABI / Calling convention
ABI는 binary interface 규칙입니다. 어떤 register에 argument를 넣는지, return value는 어디에 오는지, caller-saved와 callee-saved register는 무엇인지 정합니다. ret가 ra를 사용한다는 사실도 calling convention과 연결됩니다.
14. PC-relative addressing
PC-relative addressing은 현재 PC를 기준으로 target address를 계산하는 방식입니다. Branch, jal, auipc 기반 address 계산에서 자주 등장합니다. call과 la pseudo가 auipc를 사용하는 경우 “현재 위치에서 상대적으로 얼마나 떨어졌는가”를 표현할 수 있습니다.
15. Instruction encoding
Instruction encoding은 mnemonic을 opcode, funct3, funct7, register field, immediate field로 bit 단위 배치하는 것입니다. Pseudoinstruction은 직접 encoding하지 않습니다. 먼저 real instruction으로 expansion한 뒤, 그 real instruction을 encoding합니다.
Main Lecture Part 1
Toolchain을 단계별로 깊게 봅시다. 가장 단순한 C program을 생각해 보겠습니다.
int add1(int x) {
return x + 1;
}
이 코드는 Quellcode입니다. 사람이 함수, 변수, return이라는 개념으로 읽습니다. Compiler는 이 의미를 target architecture에 맞게 바꿉니다. RISC-V calling convention에서는 첫 번째 integer argument가 a0에 들어오고, return value도 a0에 놓입니다. 그래서 최적화된 assembly는 대략 다음처럼 될 수 있습니다.
add1:
addi a0, a0, 1
ret
여기서 초보자가 멈춰야 할 지점은 ret입니다. addi는 real instruction입니다. 그러나 ret는 pseudo입니다. Assembler는 ret를 보통 다음으로 바꿉니다.
jalr x0, ra, 0
왜 x0일까요? jalr rd, rs1, imm은 PC + 4를 rd에 저장하고 rs1 + imm으로 jump합니다. return에서는 새 return address를 저장할 필요가 없습니다. 그래서 rd를 x0으로 두어 link 값을 버립니다. 왜 ra일까요? 함수 호출 시 jal 또는 jalr가 return address를 ra에 저장했기 때문입니다. 왜 immediate가 0일까요? return target은 ra에 이미 들어 있기 때문입니다.
이제 assembler가 하는 일을 더 정확히 말할 수 있습니다. Assembler는 source assembly를 읽고, instruction mnemonic을 해석하고, pseudo를 real instruction sequence로 바꾸고, register alias를 register number로 바꾸고, immediate가 field에 맞는지 검사하고, label offset을 계산하고, object file을 만듭니다. 이 object file은 machine code를 포함하지만 최종 executable은 아닐 수 있습니다. 다른 file에 있는 symbol은 아직 unresolved일 수 있고, 최종 memory layout도 linker가 정할 수 있습니다.
Linker의 역할은 단순히 file을 붙이는 것이 아닙니다. Linker는 section을 배치합니다. .text section에는 code가, .data에는 초기화된 data가, .bss에는 초기화되지 않은 data가 들어갈 수 있습니다. 여러 object file의 section을 합치면 각 symbol의 최종 address가 정해집니다. 그 뒤 relocation entry를 보고 instruction 안의 immediate나 address 관련 field를 고칩니다. 예를 들어 call foo가 assembly source에 있었고 foo가 다른 object file에 있었다면, assembler는 “여기에 foo로 가는 call이 있다”는 정보를 남깁니다. Linker는 foo의 실제 위치를 알고 나서 auipc와 jalr immediate를 맞춥니다.
시험에서 이 흐름은 다음 식으로 물어볼 수 있습니다. “Welche Aufgabe hat der Assembler?”라고 하면 “assembly를 object code로 바꾸고 Pseudobefehle를 Basisinstruktionen으로 expand하며 symbol/relocation 정보를 만든다”라고 답해야 합니다. “Welche Aufgabe hat der Linker?”라고 하면 “object files와 libraries를 결합하고 external symbols를 resolve하며 relocation을 수행해 executable을 만든다”라고 답해야 합니다. “Compiler와 Assembler 차이”를 물으면 compiler는 high-level semantic translation과 optimization, assembler는 assembly-to-machine-code translation이라고 분리해야 합니다.
중요한 시험 습관은 “어느 단계에서 무엇이 확정되는가”입니다. C 변수의 type과 expression 의미는 compiler가 다룹니다. Register alias와 pseudo expansion은 assembler가 다룹니다. 서로 다른 object file의 symbol address는 linker가 다룹니다. Program을 memory에 실제 올리는 것은 loader와 Betriebssystem이 다룹니다. 이 선을 정확히 그으면 Theoriefragen이 훨씬 쉬워집니다.
Main Lecture Part 2
이제 Pseudoinstructions를 유형별로 봅시다. 모든 pseudo를 무작정 암기하려고 하면 부담이 큽니다. 대신 “무엇을 편하게 써 주는 pseudo인가”로 분류하면 기억이 오래갑니다.
첫 번째 유형은 register 이동과 간단한 산술입니다. mv rd, rs는 addi rd, rs, 0입니다. RISC-V에는 “register 값을 그대로 복사하는 move instruction”이 별도로 필요 없습니다. ALU가 rs + 0을 계산하면 같은 값이 rd에 들어갑니다. neg rd, rs는 sub rd, zero, rs입니다. 0에서 rs를 빼면 음수가 됩니다. not rd, rs는 xori rd, rs, -1입니다. 모든 bit를 1과 xor하면 bitwise inversion입니다. nop은 addi zero, zero, 0입니다. 아무 register도 변하지 않고 side effect가 없습니다.
두 번째 유형은 조건 비교를 편하게 쓰는 branch pseudo입니다. beqz rs, L은 beq rs, zero, L입니다. bnez rs, L은 bne rs, zero, L입니다. ble rs1, rs2, L은 보통 bge rs2, rs1, L처럼 operand를 바꾸어 표현할 수 있습니다. bgt rs1, rs2, L은 blt rs2, rs1, L입니다. 여기서 가장 흔한 실수는 operand 순서를 놓치는 것입니다. bgt t0, t1, L은 “t0 > t1이면 L”입니다. real instruction blt t1, t0, L은 “t1 < t0이면 L”이므로 같은 의미입니다.
세 번째 유형은 jump와 return입니다. j L은 jal zero, L입니다. jump는 하지만 return address는 필요 없으니 rd를 zero로 둡니다. jal L이라는 pseudo/alias를 쓰는 assembler도 있는데, 명시적 형식은 jal ra, L입니다. jr rs는 jalr zero, rs, 0입니다. ret는 jalr zero, ra, 0입니다. call symbol은 target이 멀 수 있기 때문에 일반적으로 auipc ra, ...와 jalr ra, ra, ... 조합으로 확장될 수 있습니다. 가까운 target이면 assembler/linker relaxation으로 더 짧아질 수 있지만, 시험에서는 “call은 pseudo이고 실제 jump/link instruction sequence로 바뀐다”는 점이 핵심입니다.
네 번째 유형은 상수와 주소 로딩입니다. li rd, imm은 load immediate입니다. 작은 signed 12-bit 상수면 addi rd, zero, imm 하나입니다. 큰 상수면 lui rd, upper20와 addi rd, rd, lower12가 필요합니다. 여기서 lower12의 sign bit 때문에 upper20을 보정해야 하는 경우가 있어 실제 assembler의 정확한 expansion은 immediate 값에 따라 섬세합니다. 초보 단계에서는 “작으면 addi 하나, 크면 lui plus addi 계열”이라고 잡고, encoding 문제에서는 주어진 assembler expansion을 따르거나 직접 20-bit/12-bit 분해를 조심해야 합니다. la rd, symbol은 load address입니다. absolute address가 아니라 보통 PC-relative로 auipc plus addi 또는 load relocation 조합이 나올 수 있습니다.
다섯 번째 유형은 set-on-condition pseudo입니다. seqz rd, rs는 rs가 0이면 rd를 1로 만드는 pseudo입니다. 보통 sltiu rd, rs, 1로 구현할 수 있습니다. unsigned 관점에서 rs < 1은 rs가 0일 때만 true입니다. snez rd, rs는 rs가 0이 아니면 1입니다. 보통 sltu rd, zero, rs로 구현할 수 있습니다. sltz rd, rs는 slt rd, rs, zero, sgtz rd, rs는 slt rd, zero, rs입니다. 이들은 branch가 아니라 register에 0 또는 1을 넣는 비교 결과 생성입니다.
여기서 “pseudo인지 real인지”의 경계도 다시 확인해야 합니다. mul은 많은 학생이 pseudo로 착각하지만, M-Erweiterung이 포함된 RISC-V에서는 real instruction입니다. 반면 RV32I만 허용된 환경에서는 mul 자체를 사용할 수 없거나 library call로 내려갈 수 있습니다. 시험이 “RV32I only”인지 “M extension included”인지 확인해야 합니다. Übung 4의 topic map에 M extension이 함께 언급되는 이유가 바로 이것입니다. 모든 낯선 mnemonic을 pseudo라고 생각하면 안 됩니다. ISA 확장에 속한 real instruction일 수 있습니다.
Main Lecture Part 3
이제 이 지식을 시험 풀이에 연결해 봅시다. Assembly trace 문제에서 가장 먼저 해야 할 일은 source lines를 real instruction sequence로 바꾸는 것입니다. 특히 li, la, call, ret, j, bgt, beqz가 보이면 “PC 한 칸인가, 두 칸인가, operand가 바뀌는가”를 표시합니다. 이 표시 없이 바로 register 값을 추적하면 중간에 PC와 ra가 틀어집니다.
예를 들어 다음 코드를 봅시다.
main:
li t0, 5000
call f
j done
f:
mv a0, t0
ret
done:
nop
이 코드는 source로는 6개의 명령처럼 보입니다. 하지만 실제로는 li t0, 5000이 두 instruction이 될 수 있고, call f도 두 instruction이 될 수 있고, j done은 jal zero, done, mv는 addi, ret는 jalr, nop은 addi zero, zero, 0입니다. real instruction 수는 source line 수보다 많습니다. 따라서 call이 ra에 저장하는 return address는 source line 기준이 아니라 real instruction 기준입니다. call이 두 instruction이고 그 다음 real instruction이 jal zero, done이라면 ra는 그 다음 real instruction의 address가 됩니다.
Encoding 문제에서는 pseudo를 직접 encoding하려고 하면 안 됩니다. “Encode mv t0, t1”라고 나오면 먼저 addi t0, t1, 0으로 바꾸어야 합니다. 그 다음 I-type format을 사용합니다. rd는 t0 = x5, rs1은 t1 = x6, funct3는 000, opcode는 0010011, immediate는 0입니다. “Encode ret”라면 jalr x0, x1, 0으로 바꾸고 I-type jalr format을 사용합니다. rd는 x0, rs1은 ra = x1, funct3는 000, opcode는 1100111, immediate는 0입니다.
Toolchain Theoriefrage에서는 “Pseudoinstruction은 linker가 바꾸는가?” 같은 함정이 나올 수 있습니다. 일반적으로 Pseudoinstruction expansion은 assembler의 일입니다. Linker는 address/symbol relocation을 다룹니다. 다만 실제 toolchain에서는 linker relaxation이라는 최적화가 있어 linker가 instruction sequence를 줄이거나 바꾸는 경우도 있습니다. 그러나 RO 초급 시험 답안에서는 우선 역할 분리를 명확히 해야 합니다. Assembler expands Pseudobefehle; Linker resolves external symbols and relocations.
또 하나의 중요한 함정은 “label은 assembler가 항상 해결한다”입니다. 같은 file 안의 local label branch는 assembler가 offset을 계산할 수 있습니다. 하지만 external function이나 global data symbol은 linker가 해결해야 할 수 있습니다. 그래서 object file에는 relocation entry가 들어갈 수 있습니다. object file은 이미 instruction bit를 갖고 있지만 일부 bit는 나중에 수정될 수 있습니다.
마지막으로 Pseudoinstructions는 debugging과 disassembly에서도 중요합니다. 어떤 disassembler는 real instruction을 다시 pseudo 형태로 예쁘게 보여 줄 수 있습니다. Machine code가 addi x5, x6, 0인데 disassembly가 mv t0, t1로 보일 수 있습니다. 반대로 source에서 mv를 썼는데 object dump 옵션에 따라 addi로 보일 수 있습니다. 그러므로 “보이는 text”와 “encoding된 instruction” 사이에는 표현 방식의 차이가 있다는 감각을 가져야 합니다.
Visual Block 1
다음 그림은 Toolchain의 책임 경계를 보여 주는 Markdown 친화적 도식입니다.
[Quellcode: main.c]
|
| Compiler
| - high-level 의미 분석
| - optimization
| - register allocation
v
[Assembly: main.s]
|
| Assembler
| - Pseudoinstruction expansion
| - register alias -> x-number
| - local label offset 계산
| - object code + symbol/relocation 생성
v
[Object file: main.o] + [Object file: lib.o]
|
| Linker
| - sections 배치
| - external symbol resolve
| - relocation 수행
v
[Executable: main.elf]
|
| Loader / Betriebssystem
| - memory에 적재
| - PC를 entry point로 설정
v
[CPU execution]
이 그림에서 가장 중요한 줄은 Assembler 옆의 “Pseudoinstruction expansion”입니다. mv, li, ret, j 같은 명령은 이 단계에서 real instruction으로 바뀝니다. Linker 옆의 핵심은 “external symbol resolve”와 “relocation”입니다. foo의 최종 address가 여러 object file을 합친 뒤에야 정해진다면 linker가 처리합니다.
Visual Block 2
다음 도식은 pseudo 한 줄이 실제 instruction stream에서 어떻게 길이를 바꾸는지 보여 줍니다.
Source assembly view
------------------------------------------------
0: li t0, 5000 ; pseudo, may become 2 real instructions
1: mv a0, t0 ; pseudo, usually 1 real instruction
2: ret ; pseudo, usually 1 real instruction
Possible real instruction view
------------------------------------------------
0x1000: lui t0, 0x1
0x1004: addi t0, t0, 904 ; 0x1000 + 904 = 5000
0x1008: addi a0, t0, 0 ; mv
0x100c: jalr zero, ra, 0 ; ret
PC counting rule
------------------------------------------------
Source line count: 3
Real instruction count: 4
Bytes advanced if sequential: 16
시험장에서 이 표를 머릿속으로 그리면 PC 실수가 크게 줄어듭니다. 특히 li, la, call은 한 줄이 여러 real instruction이 될 수 있다는 표시를 먼저 해야 합니다.
Worked Example 1
문제: 다음 Pseudoinstructions를 RV32I real instruction으로 바꾸고, 각 줄이 왜 같은 의미인지 설명하라.
mv t0, t1
nop
ret
beqz a0, done
bgt t0, t1, L
풀이 1단계: register alias를 떠올립니다. t0 = x5, t1 = x6, a0 = x10, ra = x1, zero = x0입니다.
풀이 2단계: 각 pseudo의 의도를 말합니다.
mv t0, t1은 t1 값을 t0에 복사합니다. RISC-V에는 별도 move가 없어도 addi t0, t1, 0이면 같은 값이 됩니다.
nop은 아무 변화도 없어야 합니다. addi zero, zero, 0은 0에 0을 더해 zero에 쓰지만, zero register는 항상 0이므로 architectural state가 바뀌지 않습니다.
ret는 ra에 들어 있는 주소로 jump합니다. jalr zero, ra, 0은 ra + 0으로 PC를 바꾸고, link 값은 zero에 써서 버립니다.
beqz a0, done은 a0가 0이면 done으로 branch합니다. real branch는 두 register를 비교하므로 beq a0, zero, done이 됩니다.
bgt t0, t1, L은 t0 > t1이면 L로 갑니다. RISC-V real branch에는 signed greater-than이 직접 없으므로 operand를 바꾸어 blt t1, t0, L로 씁니다. t1 < t0와 t0 > t1은 같은 조건입니다.
정답·검산 확인
addi t0, t1, 0
addi zero, zero, 0
jalr zero, ra, 0
beq a0, zero, done
blt t1, t0, L
검산 포인트는 두 가지입니다. 첫째, bgt expansion에서 operand 순서가 바뀌었습니다. 둘째, ret에서 rd가 ra가 아니라 zero입니다. ret는 새 return address를 저장하는 것이 아니라 기존 ra로 돌아가는 것입니다.
Worked Example 2
문제: li t0, 7과 li t0, 5000의 차이를 설명하고 가능한 expansion을 보여라.
풀이 1단계: li는 load immediate pseudo입니다. immediate 값을 register에 넣고 싶을 때 씁니다. 하지만 real instruction은 immediate bit 수가 제한되어 있습니다.
풀이 2단계: 작은 상수를 봅니다. 7은 12-bit signed immediate 범위 안에 있습니다. 따라서 다음 하나로 충분합니다.
addi t0, zero, 7
이 instruction은 zero + 7을 계산해 t0에 씁니다. 결과는 7입니다.
풀이 3단계: 큰 상수를 봅니다. 5000은 12-bit signed immediate 범위인 -2048부터 2047 밖입니다. 따라서 addi t0, zero, 5000은 불가능합니다. 한 가지 가능한 방식은 상위 부분을 lui로 만들고 하위 부분을 addi로 보정하는 것입니다.
5000은 16진수로 0x1388입니다. 이를 0x1000 + 0x388로 볼 수 있습니다. 0x388은 decimal 904이고 12-bit signed immediate 범위 안입니다. 그러므로 가능한 expansion은 다음입니다.
lui t0, 0x1
addi t0, t0, 0x388
풀이 4단계: 실제 값을 확인합니다. lui t0, 0x1은 immediate를 상위 20 bit 위치에 놓으므로 t0에 0x00001000이 들어갑니다. 그 다음 addi t0, t0, 0x388은 0x1000 + 0x388 = 0x1388 = 5000을 만듭니다.
주의할 점은 모든 큰 상수가 이렇게 단순히 잘리는 것은 아니라는 점입니다. lower 12 bit가 signed immediate로 해석될 때 음수가 되는 경우 upper 부분을 하나 올려 보정해야 할 수 있습니다. 시험에서 정확한 machine code를 요구하면 immediate 분해 규칙을 조심해야 합니다. 그러나 개념 문제에서는 “li는 pseudo이고, 작은 immediate는 addi, 큰 immediate는 lui plus addi 계열”을 확실히 말하면 됩니다.
Worked Example 3
문제: 다음 source assembly가 있다고 하자. 각 source line이 real instruction 몇 개가 될 수 있는지 표시하고, sequential 실행만 가정했을 때 PC 증가를 계산하라. 시작 PC는 0x1000이다.
li t0, 5000
mv a0, t0
nop
ret
풀이 1단계: pseudo expansion을 먼저 합니다.
li t0, 5000은 앞 예제처럼 두 instruction으로 볼 수 있습니다.
lui t0, 0x1
addi t0, t0, 0x388
mv a0, t0은 addi a0, t0, 0 하나입니다.
nop은 addi zero, zero, 0 하나입니다.
ret는 jalr zero, ra, 0 하나입니다.
풀이 2단계: real instruction address를 붙입니다. 기본 RV32I instruction은 4 byte라고 가정합니다.
0x1000: lui t0, 0x1
0x1004: addi t0, t0, 0x388
0x1008: addi a0, t0, 0
0x100c: addi zero, zero, 0
0x1010: jalr zero, ra, 0
풀이 3단계: sequential PC 증가를 계산합니다. ret는 실제로 jump하므로 “완전한 실행 경로”에서는 ra 값으로 PC가 바뀝니다. 그러나 문제에서 sequential 배치와 source line 길이를 묻는다면, real instruction은 총 5개이고 byte 길이는 20 byte입니다. ret 직전까지 순차적으로 오면 ret의 address는 0x1010입니다.
결론: source line은 4줄이지만 real instruction은 5개입니다. li 때문에 한 instruction이 추가되었습니다. 따라서 trace에서 source line만 세면 ret address를 0x100c로 착각할 수 있는데, 실제 possible address는 0x1010입니다.
Worked Example 4
문제: 다음 C file 두 개가 있다. 어떤 tool이 어떤 문제를 해결하는지 설명하라.
/* main.c */
extern int f(int);
int main() {
return f(3);
}
/* f.c */
int f(int x) {
return x + 1;
}
풀이 1단계: Compiler 단계입니다. main.c를 compiler가 보면 f가 external function이라는 선언을 압니다. main 안에서 a0에 3을 넣고 f를 call하는 assembly를 만들 수 있습니다. 대략:
main:
li a0, 3
call f
ret
f.c는 따로 compile되어 다음과 비슷한 assembly가 될 수 있습니다.
f:
addi a0, a0, 1
ret
풀이 2단계: Assembler 단계입니다. main.s의 li a0, 3은 addi a0, zero, 3으로 확장됩니다. ret는 jalr zero, ra, 0으로 확장됩니다. call f는 target symbol f가 외부에 있으므로 정확한 최종 offset을 아직 모를 수 있습니다. Assembler는 object code와 함께 relocation 정보를 남깁니다. f.s도 ret를 jalr zero, ra, 0으로 확장하고 f symbol을 object file의 symbol table에 기록합니다.
풀이 3단계: Linker 단계입니다. Linker는 main.o와 f.o를 합칩니다. f symbol의 최종 위치를 정하고, main.o 안의 call f 위치에 필요한 relocation을 적용합니다. 그 결과 executable 안의 call sequence가 정확한 target으로 갈 수 있게 됩니다.
풀이 4단계: 시험식 한 줄 답안입니다. Compiler는 각 C file을 target assembly/object로 번역하고 calling convention에 맞게 code를 만든다. Assembler는 Pseudobefehle를 Basisinstruktionen으로 expand하고 object file과 relocation 정보를 만든다. Linker는 main.o와 f.o를 합치고 external symbol f를 resolve하여 executable을 만든다.
이 예제에서 가장 중요한 함정은 f의 최종 address를 compiler가 이미 안다고 생각하는 것입니다. compiler는 f를 호출해야 한다는 의미를 알지만, 전체 executable에서 f가 어디에 놓일지는 linker가 결정합니다.
Common Wrong Mental Models
잘못된 모델 1: “Assembly에 보이는 모든 mnemonic은 CPU가 직접 실행한다.”
올바른 모델: Assembly text는 programmer와 assembler 사이의 언어입니다. CPU가 직접 보는 것은 machine code입니다. mv, ret, li, j 같은 pseudo는 assembler가 real instruction으로 바꾼 뒤에야 CPU가 실행할 수 있습니다.
잘못된 모델 2: “Pseudo 한 줄은 항상 real instruction 한 줄이다.”
올바른 모델: pseudo마다 다릅니다. mv, nop, ret, beqz는 보통 한 real instruction으로 바뀝니다. 그러나 li는 immediate 크기에 따라 한 개 또는 여러 개가 될 수 있습니다. call과 la도 symbol 거리와 relocation 방식에 따라 여러 instruction이 될 수 있습니다.
**잘못된 모델 3: “li는 그냥 register에 숫자를 넣는 real instruction이다.”**
올바른 모델: RISC-V real instruction에는 임의의 32-bit immediate를 한 번에 register에 넣는 일반 명령이 없습니다. li는 assembler pseudo이며, 작은 값은 addi, 큰 값은 lui plus addi 계열로 확장됩니다.
**잘못된 모델 4: “ret는 stack에서 주소를 꺼낸다.”**
올바른 모델: RISC-V의 ret pseudo는 jalr zero, ra, 0입니다. stack에서 ra를 복구하는 코드는 함수 epilogue에 별도로 있을 수 있습니다. ret 자체는 현재 ra 값을 PC로 사용해 jump합니다.
잘못된 모델 5: “Linker는 단순히 object file을 이어 붙인다.”
올바른 모델: Linker는 section 배치, symbol resolution, relocation을 수행합니다. 특히 external function call과 global data address는 linker가 최종 주소를 알고 고쳐야 할 수 있습니다.
잘못된 모델 6: “Assembler가 모든 label 주소를 완전히 해결한다.”
올바른 모델: local label은 assembler가 해결할 수 있는 경우가 많지만, external symbol이나 최종 배치 의존 주소는 relocation으로 남고 linker가 해결합니다.
**잘못된 모델 7: “mul은 pseudo instruction이다.”**
올바른 모델: M-Erweiterung이 포함된 RISC-V에서는 mul은 real instruction입니다. RV32I only 환경에서는 지원되지 않을 수 있습니다. pseudo인지 확장 ISA의 real instruction인지 시험 조건을 확인해야 합니다.
**잘못된 모델 8: “Disassembly에 mv라고 나오면 machine code에도 move opcode가 있다.”**
올바른 모델: Disassembler가 real instruction pattern을 pseudo 형태로 예쁘게 보여 줄 수 있습니다. addi rd, rs, 0가 mv rd, rs로 표시될 수 있습니다. 표시 형식과 encoding을 구분해야 합니다.
Exam Strategy
시험장에서 Toolchain Pseudoinstructions 문제를 만나면 다음 checklist를 그대로 적용하세요.
- 먼저 ISA 범위를 확인합니다. RV32I인지, M-Erweiterung이 포함되는지, compressed instruction을 고려하는지 봅니다.
- Assembly source에서 pseudo 후보를 표시합니다.
li, la, mv, nop, ret, j, jr, call, tail, beqz, bnez, bgt, ble, seqz, snez를 우선 의심합니다. - 각 pseudo를 real instruction으로 expansion합니다. 이때 operand 순서가 바뀌는 branch pseudo를 조심합니다.
- PC trace 문제라면 source line이 아니라 real instruction 수로 address를 붙입니다. 기본 instruction은 보통 4 byte입니다.
- Encoding 문제라면 pseudo를 직접 encoding하지 않습니다. expansion된 real instruction의 format부터 정합니다.
- Immediate 문제라면 bit 수와 signed 범위를 확인합니다.
addi는 12-bit signed immediate이고, 큰 상수는 lui plus addi가 필요할 수 있습니다. - Function call/return 문제라면
ra가 언제 쓰이고 언제 덮이는지 확인합니다. call은 return address를 만들고, ret는 ra로 jump합니다. - Toolchain Theoriefrage라면 Compiler, Assembler, Linker의 입력과 출력을 분리해서 답합니다.
- Symbol 문제라면 local label인지 external symbol인지 구분합니다. external symbol은 linker와 relocation이 핵심입니다.
- 마지막 검산으로 “CPU가 실제로 decode하는 opcode가 무엇인가?”를 물어봅니다. 이 질문에 답할 수 있으면 대부분의 함정을 피할 수 있습니다.
짧은 암기 문장도 만들어 둡시다. “Pseudo는 assembler가 풀고, symbol 주소는 linker가 고친다. CPU는 pseudo 이름을 실행하지 않고, real instruction bits를 실행한다.” 이 문장을 시험지 여백에 적어 두는 느낌으로 문제를 풀면 됩니다.
Active Recall With Answers
**Q1. mv t0, t1은 어떤 real instruction으로 확장되는가? 왜 같은 의미인가?**
정답 확인
addi t0, t1, 0입니다. t1에 0을 더하면 값이 그대로이고, 그 결과를 t0에 쓰므로 register copy가 됩니다.
**Q2. ret는 어떤 real instruction으로 확장되는가? rd가 왜 zero인가?**
정답 확인
보통 jalr zero, ra, 0입니다. return은 새 link 값을 저장할 필요가 없으므로 PC + 4 결과를 zero에 써서 버립니다. target address는 ra + 0입니다.
**Q3. li a0, 3과 li a0, 0x12345678은 왜 다르게 확장될 수 있는가?**
정답 확인
3은 12-bit signed immediate 범위에 들어가므로 addi a0, zero, 3 하나로 충분합니다. 0x12345678은 한 instruction의 immediate field에 들어가지 않으므로 lui와 addi 등 여러 instruction 조합이 필요합니다.
**Q4. bgt t0, t1, L을 real branch로 바꾸면 무엇인가?**
정답 확인
signed comparison 기준으로 보통 blt t1, t0, L입니다. t0 > t1과 t1 < t0가 같은 조건이기 때문입니다.
Q5. Compiler와 Assembler의 차이를 한 문장으로 말하라.
정답 확인
Compiler는 C 같은 high-level Quellcode를 target architecture의 low-level code로 번역하고 최적화하며, Assembler는 assembly text를 object machine code로 바꾸고 Pseudobefehle를 Basisinstruktionen으로 확장합니다.
Q6. Linker가 해결하는 대표적인 문제는 무엇인가?
정답 확인
여러 object file과 library를 합치고, external symbol reference를 실제 address로 resolve하며, relocation을 수행해 executable을 만듭니다.
Q7. Object file은 이미 machine code를 포함하는데 왜 executable이 아닐 수 있는가?
정답 확인
아직 external symbol이 unresolved일 수 있고, section의 최종 배치와 relocation이 끝나지 않았을 수 있기 때문입니다. Linker가 이 정보를 처리해야 실행 가능한 파일이 됩니다.
**Q8. nop은 어떤 instruction으로 표현할 수 있으며 architectural state가 왜 변하지 않는가?**
정답 확인
addi zero, zero, 0입니다. zero register는 항상 0이고 쓰기가 무시되므로 register나 memory 상태가 변하지 않습니다.
**Q9. call foo가 source에서 한 줄이면 PC도 항상 4 byte만 증가한다고 봐도 되는가?**
정답 확인
아닙니다. call은 pseudo이며 target 거리와 relocation 방식에 따라 auipc plus jalr 같은 여러 real instruction으로 확장될 수 있습니다. PC trace에서는 expansion 후 real instruction 수로 세야 합니다.
**Q10. mul t0, t1, t2는 항상 pseudo인가?**
정답 확인
아닙니다. M-Erweiterung이 포함된 RISC-V에서는 mul은 real instruction입니다. RV32I only라면 지원되지 않거나 다른 방식으로 처리되어야 합니다. 시험 조건의 ISA subset을 확인해야 합니다.
**Q11. la a0, msg에서 msg의 최종 주소가 나중에 바뀔 수 있는 이유는 무엇인가?**
정답 확인
msg가 어느 section의 어느 address에 배치되는지는 link 단계에서 전체 object file과 memory layout을 본 뒤 정해질 수 있습니다. 그래서 assembler가 relocation 정보를 남기고 linker가 immediate/address를 고칠 수 있습니다.
**Q12. Encoding 문제에서 mv를 바로 R-type이나 I-type으로 encoding하려 하면 왜 위험한가?**
정답 확인
mv 자체는 ISA의 real opcode가 아니기 때문입니다. 먼저 addi rd, rs, 0으로 expansion한 뒤 I-type addi format으로 encoding해야 합니다.
Source Grounding
이 강의는 RO tutor workspace의 현재 corpus map과 tutor specification을 기준으로 작성되었습니다. 직접적으로 연결되는 course material은 current:Uebung\Übung 4.pdf의 Pseudoinstructions, compiler/assembler/linker, M extension, shifts 주제와 current:Uebung\Lösung 4.pdf solution 항목입니다. RISC-V register names, instruction encodings, pseudoinstructions는 current:Uebung\RISC-V Reference.pdf가 기준 reference입니다. 더 넓은 흐름은 current:Vorlesung\Rechnerorganisation - Teil 1.pdf의 architecture, RISC-V ISA, assembler, machine language, linking 범위에 해당합니다.
튜터링 관점에서는 prompts/ro_tutor_system.md의 핵심 focus인 “RISC-V ISA, instruction formats, immediates, pseudoinstructions”, “Compiler, assembler, linker, object/executable flow”, “Manual execution of assembler, PC/register/memory state”를 따랐습니다. 문제 풀이 우선순위는 tutor_spec.md의 source priority와 mode 설명에 맞추어, 단순 요약이 아니라 trace, encoding, Theoriefrage에서 바로 쓸 수 있는 형태로 구성했습니다.