Original German, Korean translation, method, source-grounded solution, recall-answer audit, wrong-answer explanations, active recall, and source citations are separated below.
No active weak-topic rows currently map to this Aufgabe.
Subproblem learning view
소문제별 1타 강사식 풀이 교실
문제를 읽은 직후 필요한 개념을 직관적으로 잡고, 같은 순서로 손풀이를 재현하도록 구성했습니다.
Teilaufgabe
4a
2 points
Original German
Recalled German
Die C-Methode foo() war gegeben, sowie eine Uebersetzung in RISC-V welche die Aufrufskonventionen verletzt.
// C-code
foo(int a) {
int b = a + 5;
int c = b - a;
int x = bar(a, c);
return x + c;
}
// RISC-V code
foo:
addi s0, a0, 5
sub a1, a0, s0
jal ra, bar
add a0, a0, a1
jalr zero, ra
Markiere die Register an den Stellen, an denen, durch die Nichteinhaltung der RISC V Konventionen, ein falscher Wert gelesen, oder ein noch benoetigter Wert ueberschrieben wird.
한국어 문제
주어진 RISC-V 코드에서 Aufrufkonventionen 위반 때문에 잘못 읽히거나, 이후에도 필요한데 덮어써지는 register 위치를 표시하라.
한 줄 핵심
caller-saved와 callee-saved register, ra의 덮어쓰기, 함수 호출 뒤에도 필요한 live value를 함께 검사한다.
0. 초보자 개념 다리
callee-saved 물건은 원래 위치로 돌려놓고, 다음 call로 지워질 caller-saved 값은 미리 보관해야 한다.
1. 이 문제의 풀이 루틴
모든 call 위치를 표시한다.
call 뒤에도 필요한 t/a-register와 ra를 찾는다.
바뀐 s-register가 복구되는지 본다.
최초 위반 지점에 깨진 ABI 규칙을 적는다.
2. 왜 이 방법이 맞을까?
ABI는 따로 작성된 함수들이 register 보존 책임을 나누는 계약이다.
3. 시험장 실수 방지
non-leaf function에서 ra를 보존하지 않으면 ret가 원래 caller로 돌아가지 못한다.
최대 상세 해설 · 4a 깊이 학습: 호출 전후의 live value로 ABI 위반 찾기
이 강의의 도착점
주어진 RISC-V code에서 단순히 register 이름을 외워 표시하는 대신, bar 호출 전후에 살아 있어야 하는 값과 caller-/callee-saved 규칙을 연결하여 잘못 덮어쓰거나 잘못 읽는 위치를 독립적으로 판정한다.
0. 정말 아무것도 모른다면 여기서 시작
이 문제의 출발점은 ‘어떤 register가 나쁜가?’가 아니라 ‘어떤 값이 언제까지 필요하고, 그 값을 누가 보호해야 하는가?’이다. 함수 호출을 경계로 값의 생존 여부를 표시하면 위반 지점이 보인다.
foo가 bar(a,c)를 부르는 순간 foo는 Caller이고 bar는 Callee다. a0, a1, ra는 caller-saved이므로 bar 호출 뒤에도 필요한 값을 foo가 보호해야 한다. s0는 callee-saved이므로 foo가 바꾸면 자기 caller에게 돌려주기 전에 원래 값을 복구해야 한다.
이 SoSe25 문제와 답은 공식 Klausur나 공식 Lösung이 아니라 Gedächtnisprotokoll의 회상 재구성이다. 아래 ABI 규칙과 교정은 현행 Übung 3, Musterlösung 3, RISC-V Reference로 검증했지만, 당시 시험의 정확한 채점 표시는 확인되지 않았다.
주어진 sub a1,a0,s0는 c=b-a가 아니라 a-b를 계산한다. 이것은 중요한 의미 오류이지만, 4a가 묻는 Aufrufkonvention 위반과는 별도로 표시해야 한다.
1. 문제에 나오는 말부터 하나씩
Caller
다른 함수를 호출하는 쪽. foo가 bar를 호출하는 동안에는 foo가 Caller다.
Callee
호출받은 함수. 호출 동안 bar가 Callee이고, 한 단계 위의 caller가 foo를 호출했을 때는 foo 자신도 Callee다.
caller-saved register
호출받은 함수가 자유롭게 덮을 수 있는 register. 호출 뒤에도 값이 필요하면 Caller가 호출 전에 저장해야 한다. 여기서는 a0, a1, ra가 핵심이다.
callee-saved register
함수가 사용하더라도 반환 전에 진입 당시 값으로 복구해야 하는 register. s0가 이에 해당한다.
live value
현재 시점 이후에도 다시 읽어야 하는 값. c는 bar의 두 번째 인자이면서 호출 뒤 x+c에도 필요하므로 call을 가로질러 live하다.
ra
return address register. jal ra,bar는 bar에서 돌아올 주소를 새로 써서 foo가 자기 caller로 돌아갈 기존 주소를 덮는다.
semantic error
ABI 약속과 별개로 프로그램 의미를 잘못 구현한 오류. sub rd,rs1,rs2는 rs1-rs2이므로 operand 순서가 중요하다.
2. 선생님과 같이 한 칸씩 푸는 과정
먼저 C code만 본다. a로 b=a+5, c=b-a를 만들고 bar(a,c)의 반환값 x에 다시 c를 더한다. 따라서 bar 호출 뒤에도 꼭 필요한 지역 값은 c다.
Register 역할을 붙인다. 호출 직전 a0=a, a1=c이고, jal 뒤 a0=x가 된다. 그러나 a1은 caller-saved이므로 호출 뒤에도 c라고 가정할 수 없다.
첫 줄 addi s0,a0,5를 본다. foo가 callee-saved인 s0를 덮는데 기존 s0를 저장하지 않았다. 이 줄의 destination s0를 표시한다.
둘째 줄 sub a1,a0,s0에서는 a1을 호출 인자로 쓰는 것 자체는 ABI 위반이 아니다. 다만 연산은 a-s0, 즉 a-b라서 C의 b-a와 반대다. 이 오류는 ‘Konvention’ 표시와 분리한다.
jal ra,bar를 본다. jal은 새 return address를 ra에 기록한다. foo가 원래 caller로 돌아갈 주소도 아직 필요하므로, 미리 저장하지 않은 채 ra를 덮는 위치를 표시한다.
호출 뒤 add a0,a0,a1을 본다. 새 a0는 bar의 반환값 x라서 맞지만, a1은 bar가 합법적으로 바꿨을 수 있다. 따라서 이 줄에서 읽는 a1을 표시한다.
마지막 jalr zero,ra,0은 보존된 옛 ra가 아니라 직전 jal이 쓴 주소를 읽는다. 이 ra도 잘못 읽는 위치로 표시한다.
마지막으로 오류를 두 바구니로 정리한다. ABI 위반 표시는 s0, jal의 ra, 호출 후 a1, return의 ra; 별도 의미 오류는 sub의 operand 순서다.
3. 그래서 정답은 무엇인가?
회상된 code에서 Aufrufkonvention 때문에 표시할 핵심 위치는 다음과 같다.
foo:
addi (s0), a0, 5 # callee-saved s0를 저장 없이 덮음
sub a1, a0, s0 # 별도 의미 오류: a-b를 계산함
jal (ra), bar # foo의 기존 return address를 저장 없이 덮음
add a0, a0, (a1) # bar가 덮을 수 있는 caller-saved a1을 호출 뒤 읽음
jalr zero, (ra), 0 # 기존 ra가 아니라 bar 호출 뒤 주소를 읽음
따라서 convention 기준 답은 s0, ra의 overwrite/read 지점, post-call a1이다. sub a1,a0,s0의 방향 오류는 ABI 위반이 아니라 추가적인 C-to-Assembly 번역 오류다. 이는 공식 SoSe25 정답의 전사가 아니라, 회상 문제를 현행 공식 학습자료의 ABI 규칙으로 검증해 교정한 답이다.
4. 이제 정확한 개념으로 한 단계 더 깊게
Saver 분류는 register 값을 영원히 보존한다는 뜻이 아니다. caller-saved는 호출 경계를 넘을 값을 Caller가 보호하고, callee-saved는 그 register를 바꾼 Callee가 진입 값을 복구한다는 책임 분담이다.
foo는 두 역할을 동시에 가진다. 자기 caller에 대해서는 Callee이므로 s0를 보존해야 하고, bar에 대해서는 Caller이므로 a1과 ra에 남겨 둔 live value를 보호해야 한다.
ra도 caller-saved register다. leaf function은 다른 함수를 부르지 않으면 저장 없이 돌아갈 수 있지만, nested call을 수행하고 그 뒤 자기 caller로 돌아가야 하는 non-leaf function은 기존 ra를 보호해야 한다.
a0는 인자와 반환값을 겸한다. 이 문제에서는 호출 전 a0=a, 호출 후 a0=x로 역할이 의도적으로 바뀌므로, 원래 a가 호출 뒤 더 필요하지 않다면 그 변화 자체는 오류가 아니다.
a1에 c를 담아 bar의 인자로 전달하는 것은 맞다. 오류는 호출 전에 쓰는 것이 아니라, bar가 끝난 뒤에도 같은 값이 남아 있다고 가정하며 다시 읽는 것이다.
live-range 표는 ABI 문제를 기계적으로 푸는 도구다. 각 값에 대해 ‘call 뒤 필요?’, ‘현재 register가 call을 견딤?’, ‘누가 저장?’을 쓰면 암기에 덜 의존한다.
ABI correctness와 functional correctness는 서로 다른 축이다. 모든 register 보존 규칙을 지켜도 sub operand가 뒤집히면 함수 결과는 틀리고, 산술이 맞아도 ra나 s0를 깨면 호출 규약을 위반한다.
C 식에서 call 뒤 쓰이는 값을 찾는다. return x+c 때문에 c가 live-across-call임을 적는다.
register 표를 사용해 s0=callee-saved, a0/a1/ra=caller-saved를 붙인다.
call 전에는 callee-saved register의 무보존 write를 찾고, s0 destination을 표시한다.
call 명령에서는 아직 필요한 ra를 덮는지 확인하고, jal의 ra destination을 표시한다.
call 뒤에는 caller-saved register를 저장 없이 읽는지 확인하고, a1과 최종 ra read를 표시한다.
마지막 별도 검산으로 C 식과 sub rs1-rs2 방향을 비교해 semantic error를 따로 쓴다.
6. 예시와 변형 문제 연결
a=10이면 b=15, 올바른 c=5다. 회상 code의 sub a1,a0,s0는 10-15=-5를 만들어, 호출 규약을 논하기 전부터 C 의미와 다르다는 것을 즉시 확인할 수 있다.
bar가 내부에서 addi a1,zero,99를 실행해도 ABI 위반이 아니다. 그러면 foo의 post-call add a0,a0,a1은 x+c가 아니라 x+99를 계산한다. 이것이 caller-saved를 ‘보존되지 않는다고 가정’해야 하는 이유다.
foo가 s0를 전혀 쓰지 않는 대안이라면 s0 저장/복구 위반도 사라진다. 반대로 한 줄이라도 s0를 덮으면 반환 전에 원래 값을 복구해야 한다.
jal ra,bar 대신 call이 전혀 없는 leaf function이라면 진입 시 받은 ra로 바로 return할 수 있다. 이 문제는 bar를 호출하므로 그 예외에 해당하지 않는다.
7. 독일어 만점 답안 템플릿
Ich markiere s0 in addi s0,a0,5, weil s0 callee-saved ist und ohne Sichern überschrieben wird. Ich markiere ra bei jal ra,bar und beim abschließenden jalr, weil der verschachtelte Aufruf die noch benötigte Rücksprungadresse überschreibt. Ich markiere das nach bar gelesene a1, weil a1 caller-saved ist und c den Aufruf überleben muss. Zusätzlich ist sub a1,a0,s0 semantisch falsch, denn es berechnet a-b statt b-a; das ist jedoch getrennt von der Aufrufkonvention.
8. 자주 나오는 오답과 교정
s0는 saved register니까 자동으로 안전하다고 쓰기: 안전하게 만드는 책임은 그것을 바꾼 현재 함수 foo에 있다.
a1에 인자를 넣은 줄 자체를 위반으로 표시하기: a1은 정상 argument register이며, 문제는 bar 뒤에 보존됐다고 가정하는 read다.
jal은 jump만 하고 ra를 건드리지 않는다고 생각하기: jal rd,target은 rd에 다음 주소를 쓴다.
ra는 항상 Callee가 보존한다고 외우기: ABI 분류상 caller-saved이며, nested call 전에 현재 Caller가 필요한 주소를 보호한다.
a0가 호출 뒤 바뀌었다는 이유로 오류라고 표시하기: bar의 return value가 a0에 오는 것은 의도한 동작이다.
sub 방향 오류를 유일한 4a 답으로 쓰기: 중요한 기능 오류지만 질문의 Aufrufkonvention 위반과 구분해야 한다.
회상 자료의 표시를 공식 Musterlösung처럼 단정하기: 당시 원본 그림과 공식 채점 기준은 없으므로 검증된 규칙과 회상 범위를 명시해야 한다.
9. 답을 보지 않고 확인하기
foo가 bar를 호출하는 순간 Caller와 Callee는 각각 누구인가?
c는 왜 jal을 가로질러 live한 값인가?
a1에 c를 넣는 것은 언제까지 맞고, 어느 시점부터 위험한가?
jal ra,bar가 실행되면 ra에는 어떤 종류의 주소가 들어가는가?
sub a1,a0,s0는 왜 ABI 위반과 별개로 틀렸는가?
확인문제 정답 보기
foo가 Caller이고 bar가 Callee다. 동시에 foo는 자기 caller에 대해서는 Callee다.
bar(a,c)의 인자로 쓸 뿐 아니라 호출이 끝난 뒤 return x+c에서 다시 읽기 때문이다.
호출 인자를 준비하고 bar가 읽을 때까지는 맞다. bar 반환 뒤에는 caller-saved a1이 보존됐다고 가정할 수 없다.
bar 실행이 끝난 뒤 foo 내부의 다음 instruction으로 돌아올 주소가 들어간다. 그래서 foo의 원래 caller 주소는 따로 보존해야 한다.
sub rd,rs1,rs2는 rs1-rs2를 계산하므로 이 줄은 a-b를 만든다. C 식은 b-a다.
Interactive practice
ABI violation finder
호출 규약상 위험한 위치를 모두 선택하세요.
Teilaufgabe
4b
6 points
Original German
Recalled German
Die C-Methode foo() war gegeben, sowie eine Uebersetzung in RISC-V welche die Aufrufskonventionen verletzt.
// C-code
foo(int a) {
int b = a + 5;
int c = b - a;
int x = bar(a, c);
return x + c;
}
// RISC-V code
foo:
addi s0, a0, 5
sub a1, a0, s0
jal ra, bar
add a0, a0, a1
jalr zero, ra
Implementiere in Assembler, unter Einhaltung der RISCV Konventionen neu.
Normalization note
RISCV wird als RISC-V normalisiert. Der Codeblock ist RISC-V Assembly, nicht x86asm. In der erinnerten Assembly steht sub a1, a0, s0; fuer c = b - a muss die Subtraktion aber sub a1, s0, a0 oder aequivalent sein. Das ist ein zusaetzlicher semantischer Fehler, auch wenn 4a primaer nach Konventionsverletzungen fragt.
한국어 문제
같은 C 함수의 의미를 유지하면서 RISC-V ABI와 stack 규칙을 지키는 Assembly로 다시 구현하라.
한 줄 핵심
stack frame을 만들고 ra 및 필요한 s-register를 저장·복구하며, ABI return register a0로 결과를 반환한다.
0. 초보자 개념 다리
stack frame은 함수 전용 사물함이다. 들어올 때 보관하고 나갈 때 역순으로 꺼낸다.
최대 상세 해설 · 4b 깊이 학습: live value와 대칭 stack frame으로 `foo` 다시 쓰기
이 강의의 도착점
foo가 bar를 호출하면서도 원래 s0, 원래 ra, 호출 뒤 필요한 c, 그리고 sp를 모두 올바르게 보존하도록 prologue·body·epilogue를 설계하고 각 instruction의 이유를 설명한다.
0. 정말 아무것도 모른다면 여기서 시작
함수를 고치는 일은 저장 instruction을 무작정 추가하는 것이 아니다. 먼저 call 뒤에도 필요한 것을 찾고, 그 값마다 안전한 보관 장소를 하나씩 배정한 뒤, 함수 진입과 반환을 거울처럼 맞춘다.
이 문제에서 bar 호출 뒤 필요한 것은 c와 foo의 원래 return address다. c는 callee-saved s0에 둘 수 있고, 원래 ra는 stack에 둔다. 단 s0를 쓰려면 기존 s0도 stack에 저장하고 복구해야 한다.
회상된 SoSe25 답안은 공식 Lösung이 아니며, 그 답안에는 c를 caller-saved a1에 남긴 문제와 sp를 8 byte 내린 뒤 4 byte만 올린 문제가 있었다. 아래 구현은 현행 Übung 3, Musterlösung 3, RISC-V Reference의 규칙으로 교정한 학습용 답이다.
회상 문제의 C code를 그대로 구현하면 c=b-a다. 따라서 sub source 순서도 ABI와 별개로 함께 바로잡아야 한다.
1. 문제에 나오는 말부터 하나씩
Prologue
함수 시작에서 stack frame을 만들고 이 함수가 보존할 register를 저장하는 부분.
Epilogue
반환 직전에 저장한 register를 복구하고 stack frame을 없애며 caller로 돌아가는 부분.
Stack Frame
한 함수 호출 동안 stack에서 확보한 구간. 여기서는 old s0와 old ra 두 word를 저장하는 8-byte 학습 모델을 사용한다.
sp symmetry
함수 진입 때 줄인 byte 수와 반환 때 늘린 byte 수가 같아 최종 sp가 정확히 진입 값으로 돌아오는 조건.
live-across-call
다른 함수 호출 뒤에도 다시 써야 하는 값. c가 이에 해당하므로 a1 같은 caller-saved register에만 남겨 둘 수 없다.
s0-based preservation
call을 견뎌야 하는 값을 callee-saved s0에 두는 방법. 현재 함수는 이전 s0를 저장/복구하고, 호출받은 함수는 ABI에 따라 새 s0 값을 보존한다.
nested call / non-leaf function
함수 안에서 또 다른 함수를 호출하는 형태. foo는 bar를 부르므로 원래 ra를 보호해야 한다.
2. 선생님과 같이 한 칸씩 푸는 과정
함수 진입 상태를 적는다: a0=a, ra=foo를 호출한 곳으로 돌아갈 주소, s0=caller가 보존되길 기대하는 기존 값, sp=SP0.
두 word가 필요하다고 센다. old s0와 old ra를 둘 곳이므로 이 자료의 RV32 학습 모델에서는 addi sp,sp,-8로 frame을 만든다.
sw s0,0(sp)와 sw ra,4(sp)로 진입 값을 저장한다. offset을 표로 함께 쓰면 restore 순서에서 혼동이 줄어든다.
addi s0,a0,5로 b를 만든 뒤 sub s0,s0,a0로 c=b-a를 계산한다. 이제 call 뒤 필요한 c가 callee-saved s0에 있다.
addi a1,s0,0으로 bar의 두 번째 인자를 준비한다. 첫 번째 인자 a는 아직 a0에 있으므로 그대로 사용한다.
jal ra,bar를 실행한다. ra는 바뀌고 a0에는 반환값 x가 오지만, ABI를 지키는 bar는 s0를 보존하므로 s0=c는 살아 있다.
add a0,a0,s0로 x+c를 최종 return register a0에 만든다. 호출 후 a1을 읽지 않는 것이 핵심이다.
lw s0,0(sp), lw ra,4(sp)로 caller가 기대하는 register를 복구한다. 결과 a0는 복구하지 않는다.
addi sp,sp,8로 정확히 SP0에 돌아온 뒤 jalr zero,ra,0으로 caller에게 반환한다.
마지막 검산 네 개를 한다: written s register 복구, nested-call ra 복구, post-call caller-saved read 없음, 모든 sp delta의 합 0.
3. 그래서 정답은 무엇인가?
현행 학습자료의 규칙으로 교정한 한 가지 유효한 구현은 다음과 같다.
foo:
addi sp, sp, -8
sw s0, 0(sp)
sw ra, 4(sp)
addi s0, a0, 5 # s0 = b
sub s0, s0, a0 # s0 = c = b-a
addi a1, s0, 0 # bar의 두 번째 인자 c
jal ra, bar # a0 = x, s0 = c 유지
add a0, a0, s0 # a0 = x+c
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 8
jalr zero, ra, 0
frame은 0(sp)=old s0, 4(sp)=old ra이고 sp 변화는 -8+8=0이다. 호출 뒤 필요한 c는 s0에 있으므로 caller-saved a1에 의존하지 않는다. 이는 공식 SoSe25 Musterlösung이 아니라 회상 문제를 현행 공식 연습자료로 검증해 교정한 답이다.
4. 이제 정확한 개념으로 한 단계 더 깊게
보존 대상은 register 이름보다 값의 lifetime으로 결정한다. b는 call 뒤 필요 없지만 c는 필요하므로, 실제로 call을 견뎌야 하는 지역 값은 c다.
s0를 live value 보관에 쓰는 것은 ABI와 모순되지 않는다. 오히려 bar가 s0를 보존한다는 계약을 이용한다. 대신 foo도 진입 당시 old s0를 자기 caller에게 돌려줘야 한다.
ra는 jal의 destination이므로 nested call마다 바뀐다. foo 내부로 돌아올 주소와 foo의 caller로 돌아갈 주소는 서로 다르다.
argument register와 local-storage register의 역할을 분리하면 쉽다. s0가 장기 보관소이고 a1은 call 직전 전달용 복사본이다.
stack frame의 offset은 allocation 이후의 sp를 기준으로 해석한다. 같은 slot에서 저장하고 불러와야 하며, frame 해제는 모든 load가 끝난 뒤 수행한다.
sp 검산은 instruction 개수를 세는 것이 아니라 signed delta를 합하는 것이다. 여기서는 -8+8=0이어야 한다.
return value는 a0에 남겨야 한다. epilogue에서 a0를 과거 값으로 복구하면 계산한 결과를 지워 버린다.
다른 유효한 설계도 가능하다. 예를 들어 s0를 쓰지 않고 c를 stack slot에 저장할 수 있다. 채점의 핵심은 값과 ABI invariant가 보존되는지다.
5. 시험장에서 그대로 쓰는 단계별 풀이
C code에서 bar 호출 뒤 필요한 local value를 표시하고 c라고 쓴다.
register 책임표를 만든다: old s0는 Callee인 foo가, old ra와 call 뒤의 c는 Caller인 foo가 보호한다.
보관 전략을 정한다: c→s0, old s0→0(sp), old ra→4(sp).
frame size 8을 먼저 결정하고 prologue와 epilogue에 sp-=8, sp+=8을 동시에 예약한다.
b와 c의 식을 sub rd,rs1,rs2=rs1-rs2 규칙에 맞춰 구현한다.
a0=a, a1=c를 확인한 뒤 jal하고, 호출 뒤에는 a0=x, s0=c만 사용한다.
반환 직전에 old s0, old ra, old sp가 모두 복구되는지 symbolic state로 추적한다.
마지막으로 a0=x+c가 epilogue를 지나도 유지되는지 확인한다.
6. 예시와 변형 문제 연결
a=10, bar(10,5)=40이라고 가정하면 call 전 s0=5, a0=10, a1=5다. call 뒤 a0=40, s0=5이므로 add a0,a0,s0 결과는 45다. bar가 a1을 어떤 값으로 바꿔도 결과에 영향이 없다.
진입 시 s0=0x1234, ra=0x8000, sp=0x1000이었다고 하자. allocation 뒤 sp=0x0FF8, slot 0과 4에 각각 두 값을 저장한다. epilogue 뒤에는 다시 s0=0x1234, ra=0x8000, sp=0x1000이어야 한다.
잘못된 epilogue가 addi sp,sp,4라면 위 예에서 최종 sp=0x0FFC다. register load가 우연히 맞아도 caller의 stack view가 4 byte 어긋나므로 함수는 올바르지 않다.
대안으로 c를 0(sp), old ra를 4(sp)에 저장하고 call 뒤 lw t0,0(sp)로 다시 읽을 수 있다. 이때 s0를 쓰지 않으므로 old s0 저장/복구는 필요 없지만, c는 반드시 call 전에 memory에 저장되어야 한다.
7. 독일어 만점 답안 템플릿
Zuerst sichere ich die noch benötigte Rücksprungadresse und das verwendete callee-saved Register in einem symmetrischen Stackframe: sp-=8; sw s0,0(sp); sw ra,4(sp). Ich berechne c=b-a in s0, kopiere es als zweites Argument nach a1 und rufe bar auf. Nach dem Aufruf addiere ich den Rückgabewert in a0 zu dem erhaltenen c in s0. Danach stelle ich s0, ra und mit sp+=8 auch den ursprünglichen Stackpointer wieder her und kehre mit jalr zero,ra,0 zurück.
8. 자주 나오는 오답과 교정
old s0와 old ra를 저장했으니 끝이라고 생각하고 c를 계속 a1에서 읽기: bar는 a1을 덮어도 된다.
s0에 b만 두고 c는 a1에 계산하기: call 뒤 정말 필요한 값은 b가 아니라 c다.
sp-=8 뒤 sp+=4 쓰기: frame이 절반만 해제되어 caller의 stack이 망가진다.
ra를 jal 뒤에 저장하기: 그때는 이미 foo의 원래 return address가 덮인 후다.
복구 전에 sp부터 올리기: load offset이 다른 frame을 가리킬 수 있다.
sub s0,a0,s0로 쓰기: a-b가 되어 C의 b-a와 반대다.
epilogue에서 a0도 진입 값으로 복구하기: 올바른 return value x+c를 지운다.
회상 답안의 8-byte allocation과 4-byte deallocation을 그대로 베끼기: 회상 답은 공식 정답이 아니며 이 불대칭은 검증 과정에서 발견된 오류다.
9. 답을 보지 않고 확인하기
왜 c를 a1에만 둔 채 bar 뒤에 다시 읽으면 안 되는가?
foo가 s0에 c를 보관하려면 왜 old s0도 저장해야 하는가?
이 구현의 stack slot 0(sp)와 4(sp)에는 각각 무엇이 있는가?
jal ra,bar 전후에 ra의 의미는 어떻게 달라지는가?
epilogue가 끝난 뒤 반드시 성립해야 하는 register와 stack invariant를 말하라.
a=10, bar(10,5)=40이면 최종 a0는 얼마인가?
확인문제 정답 보기
a1은 caller-saved라서 bar가 합법적으로 덮을 수 있고, c는 호출 뒤 x+c에 다시 필요하기 때문이다.
s0는 callee-saved이므로 foo를 호출한 상위 함수가 진입 당시 값이 그대로 돌아오기를 기대하기 때문이다.
allocation 뒤 기준으로 0(sp)=old s0, 4(sp)=old ra다.
호출 전에는 foo의 caller로 돌아갈 주소이고, jal 직후에는 bar에서 foo 내부 다음 instruction으로 돌아올 주소다.
Wichtig: Das Gedächtnisprotokoll ist not official exam material and not official solution material. Die Loesungsbloecke daraus sind nur erinnerte Antworten und werden hier gegen aktuelle Uebung 3, aktuelle Musterloesung 3 und die RISC-V Reference geprueft.
Original German
Recalled German
Die C-Methode foo() war gegeben, sowie eine Uebersetzung in RISC-V welche die Aufrufskonventionen verletzt.
// C-code
foo(int a) {
int b = a + 5;
int c = b - a;
int x = bar(a, c);
return x + c;
}
// RISC-V code
foo:
addi s0, a0, 5
sub a1, a0, s0
jal ra, bar
add a0, a0, a1
jalr zero, ra
4a) ca. 2 Punkte
Markiere die Register an den Stellen, an denen, durch die Nichteinhaltung der RISC V Konventionen, ein falscher Wert gelesen, oder ein noch benoetigter Wert ueberschrieben wird.
4b) ca. 6 Punkte
Implementiere in Assembler, unter Einhaltung der RISCV Konventionen neu.
Normalization note
RISCV wird als RISC-V normalisiert. Der Codeblock ist RISC-V Assembly, nicht x86asm. In der erinnerten Assembly steht sub a1, a0, s0; fuer c = b - a muss die Subtraktion aber sub a1, s0, a0 oder aequivalent sein. Das ist ein zusaetzlicher semantischer Fehler, auch wenn 4a primaer nach Konventionsverletzungen fragt.
Korean Translation
foo(int a)라는 C 함수와, RISC-V Aufrufkonventionen을 어기는 Assembly 번역이 주어졌다.
foo(int a) {
int b = a + 5;
int c = b - a;
int x = bar(a, c);
return x + c;
}
foo:
addi s0, a0, 5
sub a1, a0, s0
jal ra, bar
add a0, a0, a1
jalr zero, ra
4a에서는 RISC-V Aufrufkonventionen을 지키지 않아서 잘못 읽히거나 아직 필요한 값이 덮어써지는 Register 위치를 표시한다. 4b에서는 같은 함수를 RISC-V Aufrufkonventionen에 맞게 다시 구현한다.
Caller는 함수를 호출하는 쪽이고, Callee는 호출받는 함수다. foo가 bar(a, c)를 부르는 순간 foo는 caller, bar는 callee다. s-Register는 callee가 바꿀 거면 직접 저장하고 복구해야 한다. a/t-Register는 caller-saved라서 호출 뒤에도 값이 필요하면 caller가 미리 저장해야 한다.
What Ability Is Tested
시험은 단순히 "어떤 Register가 저장돼야 하는가"만 보지 않는다. live value, 즉 함수 호출 이후에도 필요한 값을 찾는 능력을 본다. 이 문제에서 c는 bar(a, c)의 두 번째 인자로 쓰이고, 호출 뒤 return x + c에서도 다시 필요하다. 그래서 c는 bar 호출을 살아남아야 하는 live value다.
Why Students Get It Wrong
가장 흔한 실수는 a1에 c를 넣었으니 호출 뒤에도 남아 있을 것이라고 믿는 것이다. 하지만 a1은 caller-saved다. bar는 ABI를 지키더라도 a1을 자유롭게 덮을 수 있다. 두 번째 실수는 jal ra, bar가 ra를 덮는다는 점을 놓치는 것이다. 세 번째 실수는 addi sp, sp, -8로 8 bytes를 빼고 마지막에 addi sp, sp, 4만 더해서 stack이 한 word 밀린 채로 돌아가는 것이다.
Problem Interpretation
Given
Argument a arrives in a0.
bar(a, c) takes first argument in a0, second argument in a1.
bar returns x in a0.
Return value of foo must be in a0.
Recalled faulty code uses s0, a1, ra, and sp-free return.
Find
4a: 모든 Aufrufkonvention 위반 지점을 표시한다.
4b: foo를 ABI에 맞게 다시 쓴다.
Constraints
foo가 bar를 호출하므로 foo의 원래 ra는 반드시 보존되어야 한다.
foo가 s0를 쓰면, foo는 자기 caller에게 s0를 원상복구해야 한다.
c는 bar 호출 이후에도 필요하므로 a1에만 두면 안 된다.
Prologue와 epilogue의 stack 크기는 대칭이어야 한다. sp는 함수 진입 시 값으로 돌아와야 한다.
Traps
Trap
왜 위험한가
a1에 c 저장 후 그대로 사용
a1은 caller-saved라 bar가 덮어도 ABI 위반이 아니다
s0를 저장 없이 사용
s0는 callee-saved라 foo가 저장/복구해야 한다
ra 저장 없이 nested call
jal ra, bar가 caller로 돌아갈 주소를 덮는다
sp -= 8, sp += 4
stack frame이 4 bytes 남아 caller의 stack을 망가뜨린다
sub a1, a0, s0
c = b - a가 아니라 a - b를 계산한다
Required Hand-Written Intermediate State
시험장에서 먼저 이 표를 적는다.
Value
Where before bar
Needed after bar?
Safe across call?
Action
a
a0
no, only passed to bar
no
pass as a0
b = a + 5
temporary
no
not needed
may overwrite
c = b - a
should become a1 argument
yes
a1: no, s0: yes if saved
keep in saved s0 or stack
caller return address
ra
yes
ra: no
save ra
old s0
s0
caller expects it
callee must preserve
save and restore
Solving Procedure
bar 호출 전후로 살아 있어야 하는 값(live value)을 찾는다: 여기서는 c와 ra.
Register 표에서 각 Register의 Saver를 확인한다.
callee-saved Register를 쓰면 prologue에서 저장하고 epilogue에서 복구한다.
caller-saved Register 값이 호출 뒤 필요하면 stack 또는 saved register에 따로 보존한다.
jal이 ra를 덮는지 확인한다.
C 식의 방향을 Assembly와 맞춘다: c = b - a.
sp 변화량을 마지막에 정확히 되돌린다.
Detailed Solution
4a) Mark the Violations
Faulty code with the convention-relevant marks:
foo:
addi (s0), a0, 5 # writes callee-saved s0 without saving old s0
sub a1, a0, s0 # semantic error: computes a - b, not b - a
jal (ra), bar # overwrites foo's caller return address
add a0, a0, (a1) # reads caller-saved a1 after bar may have clobbered it
jalr zero, (ra), 0 # reads the wrong ra; it now points after the bar call
The sub a1, a0, s0 line is not primarily an ABI violation; it is a C-to-Assembly translation error. If the exam wanted only Aufrufkonventionen, grade it separately as an extra correctness issue.
4b) Correct Implementation, s0-Based
This version keeps c in s0 across the call. That is valid because s0 is callee-saved; therefore foo must save old s0 before using it, and bar must preserve s0 if it uses it.
foo:
addi sp, sp, -8 # frame: 0(sp)=old s0, 4(sp)=old ra
sw s0, 0(sp)
sw ra, 4(sp)
addi s0, a0, 5 # s0 = b = a + 5
sub s0, s0, a0 # s0 = c = b - a
addi a1, s0, 0 # second argument: c
# first argument a is still in a0
jal ra, bar # a0 = x; s0 still holds c by ABI
add a0, a0, s0 # return x + c
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 8
jalr zero, ra, 0
Frame layout:
Offset from sp after allocation
Content
Why
0(sp)
old s0
foo uses callee-saved s0
4(sp)
old ra
foo performs nested call to bar
Prologue/epilogue symmetry:
Step
Instruction
sp relation
Entry
before prologue
sp = SP0
Allocate
addi sp, sp, -8
sp = SP0 - 8
Restore
addi sp, sp, 8
sp = SP0
Alternative Correct Implementation, Stack Slot for c
이 버전은 s0를 아예 쓰지 않고 c를 stack에 저장한다. 따라서 s0 save/restore가 필요 없다. 단, ra와 c 두 word가 필요하므로 frame은 여전히 8 bytes다.
foo:
addi sp, sp, -8 # frame: 0(sp)=c, 4(sp)=old ra
sw ra, 4(sp)
addi t0, a0, 5 # t0 = b
sub t0, t0, a0 # t0 = c
sw t0, 0(sp) # c must survive bar
addi a1, t0, 0 # bar's second argument
jal ra, bar
lw t0, 0(sp) # reload c; t0 is caller-saved
add a0, a0, t0 # x + c
lw ra, 4(sp)
addi sp, sp, 8
jalr zero, ra, 0
둘 다 맞다. 시험 답안으로는 첫 번째가 recalled code의 의도(s0 사용)를 가장 직접적으로 고친 형태다.
s0 must not be overwritten without saving: verified by RISC-V Reference and Uebung 3.1b.
ra must be saved before a nested call when the old return address is still needed: verified by Uebung 3.2a and 3.4a.
a1 cannot be trusted after bar: verified by RISC-V Reference (a0-a1 are Caller-saved) and Uebung 3.1c's rule for caller-saved temporaries.
The recalled repair correctly changes the arithmetic direction to sub a1, s0, a0.
Incomplete or Wrong Parts
Issue
First error
Violated rule
Correct answer
Original code computes wrong c
sub a1, a0, s0
C says c = b - a
use sub ..., s0, a0
Recalled repair loses c
add a0, a0, a1 after jal
a1 is caller-saved
keep c in saved s0 or stack
Recalled repair corrupts sp
addi sp, sp, 4 after sp -= 8
callee must restore sp
use addi sp, sp, 8
Recalled repair underuses saved s0
stores b in s0, then c in a1
live c is the value that must survive call
put c in s0, or store it on stack
Source Verification Status
Verified against current material. Uebung 3 Musterloesung pages 1-3 states the s0-s11 preservation rule, when caller-saved values must be saved, and what jal places in ra. Pages 4-6 show saving a0 and ra for foo3, exactly the same pattern as "value needed after call plus return address". Pages 7-9 show recursive code saving argument/result and ra on stack. RISC-V Reference pages 1-2 confirms the register saver classes.
Wrong-Answer Explanations
Wrong answer
Why a student chooses it
Why it is wrong
Violated rule
Fast check
Correct approach
Only save ra, leave s0 alone
They see the nested call but ignore callee-saved registers
Caller may have had a live value in s0 before calling foo
s0 is callee-saved
Did this function write any s register? If yes, save/restore it
Save old s0 or avoid s0 entirely
Save s0 and ra, but use a1 after bar
They think arguments remain unchanged
bar may overwrite a1; ABI still valid
a1 is caller-saved
Is a1 used after jal? Then suspect bug
Put c in saved s0 or stack
addi sp, sp, -8 then addi sp, sp, 4
They count only one restored register mentally
Stack pointer returns to SP0 - 4, not SP0
sp must be restored
Sum all sp deltas: must be 0
Use addi sp, sp, 8
sub a1, a0, s0
They read sub rd, rs1, rs2 too fast
Computes a - b = -5, not b - a = 5
C expression order
Substitute a=10: expected 5, got -5
sub a1, s0, a0 or addi a1, zero, 5 if optimizing
Save c in t0 before bar, then read t0 after
They know a1 is unsafe but pick another caller-saved register
t0 is caller-saved too
caller-saved values do not survive calls
Is it an a or t register across jal? Unsafe
Save to stack or saved s register
Exam-Room Method
Time budget: 2 minutes for 4a, 6 minutes for 4b.
First table to write:
Register
Saver
In this task
ra
caller
save before jal bar because foo must return later
s0
callee
if used, save old value and restore
a0
caller
input a, later return x; no need to preserve old a after preparing bar
a1
caller
argument c; do not read after bar unless reloaded
Partial-credit work:
Even if the final code is incomplete, mark s0, ra, and post-call a1.
Draw the stack frame with offsets.
Write sp -= frame_size and immediately reserve the matching sp += frame_size line.
State explicitly: c is live across the call.
Last check:
Does every jal that needs the old ra have ra saved?
Does every written s register get restored?
Is any a/t register read after a call without being saved?
Does sum(sp deltas) = 0?
Does sub implement b - a, not a - b?
Active Recall
Questions
Concept check: s0와 a1 중 어느 쪽이 callee-saved이고, 어느 쪽이 caller-saved인가?
Trace: faulty code에서 jal ra, bar가 실행된 직후 ra는 무엇을 가리키는가? 그 상태에서 마지막 jalr zero, ra를 실행하면 왜 문제가 생기는가?
Hand calculation: a = 10이면 b, c, bar 호출 인자들은 무엇이어야 하는가? faulty sub a1, a0, s0는 c를 몇으로 만든는가?
Transfer variant: return bar(a, c);처럼 호출 뒤 c를 더 이상 쓰지 않는다면 c를 call across로 보존해야 하는가?