SoSe25 Exam Overview

Aufgabe 4 · 8 points · trace

Aufgabe 4 - RISC-V Aufrufkonventionen

Original German, Korean translation, method, source-grounded solution, recall-answer audit, wrong-answer explanations, active recall, and source citations are separated below.

complete

Related Concepts and Current Sources

Weak-topic hook

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. 이 문제의 풀이 루틴

  1. 모든 call 위치를 표시한다.
  2. call 뒤에도 필요한 t/a-register와 ra를 찾는다.
  3. 바뀐 s-register가 복구되는지 본다.
  4. 최초 위반 지점에 깨진 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가 나쁜가?’가 아니라 ‘어떤 값이 언제까지 필요하고, 그 값을 누가 보호해야 하는가?’이다. 함수 호출을 경계로 값의 생존 여부를 표시하면 위반 지점이 보인다.
  • foobar(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,s0c=b-a가 아니라 a-b를 계산한다. 이것은 중요한 의미 오류이지만, 4a가 묻는 Aufrufkonvention 위반과는 별도로 표시해야 한다.

1. 문제에 나오는 말부터 하나씩

Caller
다른 함수를 호출하는 쪽. foobar를 호출하는 동안에는 foo가 Caller다.
Callee
호출받은 함수. 호출 동안 bar가 Callee이고, 한 단계 위의 caller가 foo를 호출했을 때는 foo 자신도 Callee다.
caller-saved register
호출받은 함수가 자유롭게 덮을 수 있는 register. 호출 뒤에도 값이 필요하면 Caller가 호출 전에 저장해야 한다. 여기서는 a0, a1, ra가 핵심이다.
callee-saved register
함수가 사용하더라도 반환 전에 진입 당시 값으로 복구해야 하는 register. s0가 이에 해당한다.
live value
현재 시점 이후에도 다시 읽어야 하는 값. cbar의 두 번째 인자이면서 호출 뒤 x+c에도 필요하므로 call을 가로질러 live하다.
ra
return address register. jal ra,barbar에서 돌아올 주소를 새로 써서 foo가 자기 caller로 돌아갈 기존 주소를 덮는다.
semantic error
ABI 약속과 별개로 프로그램 의미를 잘못 구현한 오류. sub rd,rs1,rs2rs1-rs2이므로 operand 순서가 중요하다.

2. 선생님과 같이 한 칸씩 푸는 과정

  1. 먼저 C code만 본다. ab=a+5, c=b-a를 만들고 bar(a,c)의 반환값 x에 다시 c를 더한다. 따라서 bar 호출 뒤에도 꼭 필요한 지역 값은 c다.
  2. Register 역할을 붙인다. 호출 직전 a0=a, a1=c이고, jala0=x가 된다. 그러나 a1은 caller-saved이므로 호출 뒤에도 c라고 가정할 수 없다.
  3. 첫 줄 addi s0,a0,5를 본다. foo가 callee-saved인 s0를 덮는데 기존 s0를 저장하지 않았다. 이 줄의 destination s0를 표시한다.
  4. 둘째 줄 sub a1,a0,s0에서는 a1을 호출 인자로 쓰는 것 자체는 ABI 위반이 아니다. 다만 연산은 a-s0, 즉 a-b라서 C의 b-a와 반대다. 이 오류는 ‘Konvention’ 표시와 분리한다.
  5. jal ra,bar를 본다. jal은 새 return address를 ra에 기록한다. foo가 원래 caller로 돌아갈 주소도 아직 필요하므로, 미리 저장하지 않은 채 ra를 덮는 위치를 표시한다.
  6. 호출 뒤 add a0,a0,a1을 본다. 새 a0bar의 반환값 x라서 맞지만, a1bar가 합법적으로 바꿨을 수 있다. 따라서 이 줄에서 읽는 a1을 표시한다.
  7. 마지막 jalr zero,ra,0은 보존된 옛 ra가 아니라 직전 jal이 쓴 주소를 읽는다. 이 ra도 잘못 읽는 위치로 표시한다.
  8. 마지막으로 오류를 두 바구니로 정리한다. ABI 위반 표시는 s0, jalra, 호출 후 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이므로 a1ra에 남겨 둔 live value를 보호해야 한다.
  • ra도 caller-saved register다. leaf function은 다른 함수를 부르지 않으면 저장 없이 돌아갈 수 있지만, nested call을 수행하고 그 뒤 자기 caller로 돌아가야 하는 non-leaf function은 기존 ra를 보호해야 한다.
  • a0는 인자와 반환값을 겸한다. 이 문제에서는 호출 전 a0=a, 호출 후 a0=x로 역할이 의도적으로 바뀌므로, 원래 a가 호출 뒤 더 필요하지 않다면 그 변화 자체는 오류가 아니다.
  • a1c를 담아 bar의 인자로 전달하는 것은 맞다. 오류는 호출 전에 쓰는 것이 아니라, bar가 끝난 뒤에도 같은 값이 남아 있다고 가정하며 다시 읽는 것이다.
  • live-range 표는 ABI 문제를 기계적으로 푸는 도구다. 각 값에 대해 ‘call 뒤 필요?’, ‘현재 register가 call을 견딤?’, ‘누가 저장?’을 쓰면 암기에 덜 의존한다.
  • ABI correctness와 functional correctness는 서로 다른 축이다. 모든 register 보존 규칙을 지켜도 sub operand가 뒤집히면 함수 결과는 틀리고, 산술이 맞아도 ras0를 깨면 호출 규약을 위반한다.

5. 시험장에서 그대로 쓰는 단계별 풀이

  1. code의 각 register write와 read에 밑줄을 긋고 jal 앞뒤에 세로선을 그린다.
  2. C 식에서 call 뒤 쓰이는 값을 찾는다. return x+c 때문에 c가 live-across-call임을 적는다.
  3. register 표를 사용해 s0=callee-saved, a0/a1/ra=caller-saved를 붙인다.
  4. call 전에는 callee-saved register의 무보존 write를 찾고, s0 destination을 표시한다.
  5. call 명령에서는 아직 필요한 ra를 덮는지 확인하고, jalra destination을 표시한다.
  6. call 뒤에는 caller-saved register를 저장 없이 읽는지 확인하고, a1과 최종 ra read를 표시한다.
  7. 마지막 별도 검산으로 C 식과 sub rs1-rs2 방향을 비교해 semantic error를 따로 쓴다.

6. 예시와 변형 문제 연결

  • a=10이면 b=15, 올바른 c=5다. 회상 code의 sub a1,a0,s010-15=-5를 만들어, 호출 규약을 논하기 전부터 C 의미와 다르다는 것을 즉시 확인할 수 있다.
  • bar가 내부에서 addi a1,zero,99를 실행해도 ABI 위반이 아니다. 그러면 foo의 post-call add a0,a0,a1x+c가 아니라 x+99를 계산한다. 이것이 caller-saved를 ‘보존되지 않는다고 가정’해야 하는 이유다.
  • foos0를 전혀 쓰지 않는 대안이라면 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,targetrd에 다음 주소를 쓴다.
  • ra는 항상 Callee가 보존한다고 외우기: ABI 분류상 caller-saved이며, nested call 전에 현재 Caller가 필요한 주소를 보호한다.
  • a0가 호출 뒤 바뀌었다는 이유로 오류라고 표시하기: bar의 return value가 a0에 오는 것은 의도한 동작이다.
  • sub 방향 오류를 유일한 4a 답으로 쓰기: 중요한 기능 오류지만 질문의 Aufrufkonvention 위반과 구분해야 한다.
  • 회상 자료의 표시를 공식 Musterlösung처럼 단정하기: 당시 원본 그림과 공식 채점 기준은 없으므로 검증된 규칙과 회상 범위를 명시해야 한다.

9. 답을 보지 않고 확인하기

  1. foobar를 호출하는 순간 Caller와 Callee는 각각 누구인가?
  2. c는 왜 jal을 가로질러 live한 값인가?
  3. a1c를 넣는 것은 언제까지 맞고, 어느 시점부터 위험한가?
  4. jal ra,bar가 실행되면 ra에는 어떤 종류의 주소가 들어가는가?
  5. sub a1,a0,s0는 왜 ABI 위반과 별개로 틀렸는가?
확인문제 정답 보기
  1. foo가 Caller이고 bar가 Callee다. 동시에 foo는 자기 caller에 대해서는 Callee다.
  2. bar(a,c)의 인자로 쓸 뿐 아니라 호출이 끝난 뒤 return x+c에서 다시 읽기 때문이다.
  3. 호출 인자를 준비하고 bar가 읽을 때까지는 맞다. bar 반환 뒤에는 caller-saved a1이 보존됐다고 가정할 수 없다.
  4. bar 실행이 끝난 뒤 foo 내부의 다음 instruction으로 돌아올 주소가 들어간다. 그래서 foo의 원래 caller 주소는 따로 보존해야 한다.
  5. sub rd,rs1,rs2rs1-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은 함수 전용 사물함이다. 들어올 때 보관하고 나갈 때 역순으로 꺼낸다.

1. 이 문제의 풀이 루틴

  1. 필요 공간만큼 aligned sp를 내린다.
  2. ra와 사용하는 s-register를 store한다.
  3. call 사이의 live value를 s-register/stack에 둔다.
  4. a0에 결과를 놓고 저장값·sp를 복구한 뒤 ret한다.

2. 왜 이 방법이 맞을까?

대칭인 prologue/epilogue가 caller의 상태와 return address를 보존한다.

3. 시험장 실수 방지

옛 frame의 값을 load한 뒤 sp를 복구한다.

최대 상세 해설 · 4b 깊이 학습: live value와 대칭 stack frame으로 `foo` 다시 쓰기

이 강의의 도착점

foobar를 호출하면서도 원래 s0, 원래 ra, 호출 뒤 필요한 c, 그리고 sp를 모두 올바르게 보존하도록 prologue·body·epilogue를 설계하고 각 instruction의 이유를 설명한다.

0. 정말 아무것도 모른다면 여기서 시작

  • 함수를 고치는 일은 저장 instruction을 무작정 추가하는 것이 아니다. 먼저 call 뒤에도 필요한 것을 찾고, 그 값마다 안전한 보관 장소를 하나씩 배정한 뒤, 함수 진입과 반환을 거울처럼 맞춘다.
  • 이 문제에서 bar 호출 뒤 필요한 것은 cfoo의 원래 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
함수 안에서 또 다른 함수를 호출하는 형태. foobar를 부르므로 원래 ra를 보호해야 한다.

2. 선생님과 같이 한 칸씩 푸는 과정

  1. 함수 진입 상태를 적는다: a0=a, ra=foo를 호출한 곳으로 돌아갈 주소, s0=caller가 보존되길 기대하는 기존 값, sp=SP0.
  2. 두 word가 필요하다고 센다. old s0와 old ra를 둘 곳이므로 이 자료의 RV32 학습 모델에서는 addi sp,sp,-8로 frame을 만든다.
  3. sw s0,0(sp)sw ra,4(sp)로 진입 값을 저장한다. offset을 표로 함께 쓰면 restore 순서에서 혼동이 줄어든다.
  4. addi s0,a0,5b를 만든 뒤 sub s0,s0,a0c=b-a를 계산한다. 이제 call 뒤 필요한 c가 callee-saved s0에 있다.
  5. addi a1,s0,0으로 bar의 두 번째 인자를 준비한다. 첫 번째 인자 a는 아직 a0에 있으므로 그대로 사용한다.
  6. jal ra,bar를 실행한다. ra는 바뀌고 a0에는 반환값 x가 오지만, ABI를 지키는 bars0를 보존하므로 s0=c는 살아 있다.
  7. add a0,a0,s0x+c를 최종 return register a0에 만든다. 호출 후 a1을 읽지 않는 것이 핵심이다.
  8. lw s0,0(sp), lw ra,4(sp)로 caller가 기대하는 register를 복구한다. 결과 a0는 복구하지 않는다.
  9. addi sp,sp,8로 정확히 SP0에 돌아온 뒤 jalr zero,ra,0으로 caller에게 반환한다.
  10. 마지막 검산 네 개를 한다: 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이다. 호출 뒤 필요한 cs0에 있으므로 caller-saved a1에 의존하지 않는다. 이는 공식 SoSe25 Musterlösung이 아니라 회상 문제를 현행 공식 연습자료로 검증해 교정한 답이다.

4. 이제 정확한 개념으로 한 단계 더 깊게

  • 보존 대상은 register 이름보다 값의 lifetime으로 결정한다. b는 call 뒤 필요 없지만 c는 필요하므로, 실제로 call을 견뎌야 하는 지역 값은 c다.
  • s0를 live value 보관에 쓰는 것은 ABI와 모순되지 않는다. 오히려 bars0를 보존한다는 계약을 이용한다. 대신 foo도 진입 당시 old s0를 자기 caller에게 돌려줘야 한다.
  • rajal의 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. 시험장에서 그대로 쓰는 단계별 풀이

  1. C code에서 bar 호출 뒤 필요한 local value를 표시하고 c라고 쓴다.
  2. register 책임표를 만든다: old s0는 Callee인 foo가, old ra와 call 뒤의 c는 Caller인 foo가 보호한다.
  3. 보관 전략을 정한다: c→s0, old s0→0(sp), old ra→4(sp).
  4. frame size 8을 먼저 결정하고 prologue와 epilogue에 sp-=8, sp+=8을 동시에 예약한다.
  5. bc의 식을 sub rd,rs1,rs2=rs1-rs2 규칙에 맞춰 구현한다.
  6. a0=a, a1=c를 확인한 뒤 jal하고, 호출 뒤에는 a0=x, s0=c만 사용한다.
  7. 반환 직전에 old s0, old ra, old sp가 모두 복구되는지 symbolic state로 추적한다.
  8. 마지막으로 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다. bara1을 어떤 값으로 바꿔도 결과에 영향이 없다.
  • 진입 시 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 어긋나므로 함수는 올바르지 않다.
  • 대안으로 c0(sp), old ra4(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에서 읽기: bara1을 덮어도 된다.
  • s0b만 두고 ca1에 계산하기: call 뒤 정말 필요한 값은 b가 아니라 c다.
  • sp-=8sp+=4 쓰기: frame이 절반만 해제되어 caller의 stack이 망가진다.
  • rajal 뒤에 저장하기: 그때는 이미 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. 답을 보지 않고 확인하기

  1. ca1에만 둔 채 bar 뒤에 다시 읽으면 안 되는가?
  2. foos0c를 보관하려면 왜 old s0도 저장해야 하는가?
  3. 이 구현의 stack slot 0(sp)4(sp)에는 각각 무엇이 있는가?
  4. jal ra,bar 전후에 ra의 의미는 어떻게 달라지는가?
  5. epilogue가 끝난 뒤 반드시 성립해야 하는 register와 stack invariant를 말하라.
  6. a=10, bar(10,5)=40이면 최종 a0는 얼마인가?
확인문제 정답 보기
  1. a1은 caller-saved라서 bar가 합법적으로 덮을 수 있고, c는 호출 뒤 x+c에 다시 필요하기 때문이다.
  2. s0는 callee-saved이므로 foo를 호출한 상위 함수가 진입 당시 값이 그대로 돌아오기를 기대하기 때문이다.
  3. allocation 뒤 기준으로 0(sp)=old s0, 4(sp)=old ra다.
  4. 호출 전에는 foo의 caller로 돌아갈 주소이고, jal 직후에는 bar에서 foo 내부 다음 instruction으로 돌아올 주소다.
  5. s0ra는 진입 값으로 복구되고, spSP0로 돌아오며, a0에는 계산된 return value가 남아야 한다.
  6. c=15-10=5이고 x=40이므로 최종 a0=45다.

Interactive practice

Stack frame stepper

함수 호출 전후의 sp, ra, s0 보존 상태를 단계별로 확인하세요.

Intro

Metadata

FeldInhalt
NummerAufgabe 4
TitelRISC-V Aufrufkonventionen
Punkte8 Punkte
Empfohlene Zeit8 Minuten
Tutor modetrace
KonzepteAufrufkonvention, caller-saved, callee-saved, ra, s0, a0/a1, Stack Frame, Prologue/Epilogue
Recall-source confidencemittel: Code und Aufgabenform sind klar, aber die rekonstruierte Loesung enthaelt nachweisbare Fehler
Verification sourcesGedächtnisprotokoll Rechnerorganisation SoSe25.md#aufgabe-4, Uebung\Übung 3.pdf pages 1-4, Uebung\Übung 3 Musterlösung.pdf pages 1-9, Uebung\RISC-V Reference.pdf pages 1-2

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에 맞게 다시 구현한다.

Concept Lesson

Prerequisites

이 문제를 풀려면 세 가지 약속을 먼저 외워야 한다.

RegisterABI name역할Saver
x1raReturn addressCaller
x2spStack pointerCallee
x8s0/fpSaved register / Frame pointerCallee
x10-x11a0-a1Function arguments / return valuesCaller
x5-x7, x28-x31t0-t6Temporary registersCaller

근거: RISC-V Reference의 Register Names 표는 ra를 Caller-saved, sps0/fp를 Callee-saved, a0-a1을 Function arguments / Return values이면서 Caller-saved로 둔다 (Uebung\RISC-V Reference.pdf, pages 1-2).

Key Terms

Caller는 함수를 호출하는 쪽이고, Callee는 호출받는 함수다. foobar(a, c)를 부르는 순간 foo는 caller, bar는 callee다. s-Register는 callee가 바꿀 거면 직접 저장하고 복구해야 한다. a/t-Register는 caller-saved라서 호출 뒤에도 값이 필요하면 caller가 미리 저장해야 한다.

What Ability Is Tested

시험은 단순히 "어떤 Register가 저장돼야 하는가"만 보지 않는다. live value, 즉 함수 호출 이후에도 필요한 값을 찾는 능력을 본다. 이 문제에서 cbar(a, c)의 두 번째 인자로 쓰이고, 호출 뒤 return x + c에서도 다시 필요하다. 그래서 cbar 호출을 살아남아야 하는 live value다.

Why Students Get It Wrong

가장 흔한 실수는 a1c를 넣었으니 호출 뒤에도 남아 있을 것이라고 믿는 것이다. 하지만 a1은 caller-saved다. bar는 ABI를 지키더라도 a1을 자유롭게 덮을 수 있다. 두 번째 실수는 jal ra, barra를 덮는다는 점을 놓치는 것이다. 세 번째 실수는 addi sp, sp, -8로 8 bytes를 빼고 마지막에 addi sp, sp, 4만 더해서 stack이 한 word 밀린 채로 돌아가는 것이다.

Problem Interpretation

Given

Find

Constraints

Traps

Trap왜 위험한가
a1c 저장 후 그대로 사용a1은 caller-saved라 bar가 덮어도 ABI 위반이 아니다
s0를 저장 없이 사용s0는 callee-saved라 foo가 저장/복구해야 한다
ra 저장 없이 nested calljal ra, bar가 caller로 돌아갈 주소를 덮는다
sp -= 8, sp += 4stack frame이 4 bytes 남아 caller의 stack을 망가뜨린다
sub a1, a0, s0c = b - a가 아니라 a - b를 계산한다

Required Hand-Written Intermediate State

시험장에서 먼저 이 표를 적는다.

ValueWhere before barNeeded after bar?Safe across call?Action
aa0no, only passed to barnopass as a0
b = a + 5temporarynonot neededmay overwrite
c = b - ashould become a1 argumentyesa1: no, s0: yes if savedkeep in saved s0 or stack
caller return addressrayesra: nosave ra
old s0s0caller expects itcallee must preservesave and restore

Solving Procedure

  1. bar 호출 전후로 살아 있어야 하는 값(live value)을 찾는다: 여기서는 cra.
  2. Register 표에서 각 Register의 Saver를 확인한다.
  3. callee-saved Register를 쓰면 prologue에서 저장하고 epilogue에서 복구한다.
  4. caller-saved Register 값이 호출 뒤 필요하면 stack 또는 saved register에 따로 보존한다.
  5. jalra를 덮는지 확인한다.
  6. C 식의 방향을 Assembly와 맞춘다: c = b - a.
  7. 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 allocationContentWhy
0(sp)old s0foo uses callee-saved s0
4(sp)old rafoo performs nested call to bar

Prologue/epilogue symmetry:

StepInstructionsp relation
Entrybefore prologuesp = SP0
Allocateaddi sp, sp, -8sp = SP0 - 8
Restoreaddi sp, sp, 8sp = SP0

Alternative Correct Implementation, Stack Slot for c

이 버전은 s0를 아예 쓰지 않고 c를 stack에 저장한다. 따라서 s0 save/restore가 필요 없다. 단, rac 두 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 사용)를 가장 직접적으로 고친 형태다.

Recall-Answer Audit

What the File Claims

The recalled 4a answer marks:

The recalled 4b repair is:

addi sp, sp, -8
sw s0, 0(sp)
sw ra, 4(sp)
addi s0, a0, 5
sub a1, s0, a0
jal ra, bar
add a0, a0, a1
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 4
jalr zero, ra, 0

Correct Parts

Incomplete or Wrong Parts

IssueFirst errorViolated ruleCorrect answer
Original code computes wrong csub a1, a0, s0C says c = b - ause sub ..., s0, a0
Recalled repair loses cadd a0, a0, a1 after jala1 is caller-savedkeep c in saved s0 or stack
Recalled repair corrupts spaddi sp, sp, 4 after sp -= 8callee must restore spuse addi sp, sp, 8
Recalled repair underuses saved s0stores b in s0, then c in a1live c is the value that must survive callput 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 answerWhy a student chooses itWhy it is wrongViolated ruleFast checkCorrect approach
Only save ra, leave s0 aloneThey see the nested call but ignore callee-saved registersCaller may have had a live value in s0 before calling foos0 is callee-savedDid this function write any s register? If yes, save/restore itSave old s0 or avoid s0 entirely
Save s0 and ra, but use a1 after barThey think arguments remain unchangedbar may overwrite a1; ABI still valida1 is caller-savedIs a1 used after jal? Then suspect bugPut c in saved s0 or stack
addi sp, sp, -8 then addi sp, sp, 4They count only one restored register mentallyStack pointer returns to SP0 - 4, not SP0sp must be restoredSum all sp deltas: must be 0Use addi sp, sp, 8
sub a1, a0, s0They read sub rd, rs1, rs2 too fastComputes a - b = -5, not b - a = 5C expression orderSubstitute a=10: expected 5, got -5sub a1, s0, a0 or addi a1, zero, 5 if optimizing
Save c in t0 before bar, then read t0 afterThey know a1 is unsafe but pick another caller-saved registert0 is caller-saved toocaller-saved values do not survive callsIs it an a or t register across jal? UnsafeSave to stack or saved s register

Exam-Room Method

Time budget: 2 minutes for 4a, 6 minutes for 4b.

First table to write:

RegisterSaverIn this task
racallersave before jal bar because foo must return later
s0calleeif used, save old value and restore
a0callerinput a, later return x; no need to preserve old a after preparing bar
a1callerargument c; do not read after bar unless reloaded

Partial-credit work:

Last check:

  1. Does every jal that needs the old ra have ra saved?
  2. Does every written s register get restored?
  3. Is any a/t register read after a call without being saved?
  4. Does sum(sp deltas) = 0?
  5. Does sub implement b - a, not a - b?

Active Recall

Questions

  1. Concept check: s0a1 중 어느 쪽이 callee-saved이고, 어느 쪽이 caller-saved인가?
  2. Trace: faulty code에서 jal ra, bar가 실행된 직후 ra는 무엇을 가리키는가? 그 상태에서 마지막 jalr zero, ra를 실행하면 왜 문제가 생기는가?
  3. Hand calculation: a = 10이면 b, c, bar 호출 인자들은 무엇이어야 하는가? faulty sub a1, a0, s0c를 몇으로 만든는가?
  4. Transfer variant: return bar(a, c);처럼 호출 뒤 c를 더 이상 쓰지 않는다면 c를 call across로 보존해야 하는가?

Answers

  1. s0는 callee-saved다. a1은 caller-saved이며 argument register다.
  2. rabar가 끝난 뒤 돌아올 add a0, a0, a1 주소를 가리킨다. 마지막 return에서도 그 ra를 쓰면 caller로 돌아가지 않고 foo 내부로 되돌아가므로 잘못된 흐름이 된다.
  3. b = 15, c = 5, bar 인자는 a0 = 10, a1 = 5이어야 한다. faulty sub a1, a0, s010 - 15 = -5를 만든다.
  4. 아니오. 호출 뒤 필요하지 않다면 a1에 인자로 넣고 바로 jal해도 된다. 단 ra는 여전히 저장해야 할 수 있다.

Sources

SourceLocatorUsed for
Gedächtnisprotokoll Rechnerorganisation SoSe25.md#aufgabe-4, lines 174-233; chunks aufgabe-4, aufgabe-4-teil-4a, aufgabe-4-teil-4brecalled problem and recalled answer audit
Uebung\Übung 3.pdfpages 1-3caller/callee roles, s-register rule, t/caller-saved rule, jal/ra, simple saving tasks
Uebung\Übung 3.pdfpages 4-4recursive ABI violation task analogous to marking overwritten s0, s1, ra
Uebung\Übung 3 Musterlösung.pdfpages 1-3official current solution for caller/callee, s0-s11, t0-t6, ra, and foo1 saving ra
Uebung\Übung 3 Musterlösung.pdfpages 4-6official current foo3 pattern: save live argument and ra across call; stack frame restored symmetrically
Uebung\Übung 3 Musterlösung.pdfpages 7-9official current recursive pattern: save a0, ra, intermediate result; restore sp
Uebung\RISC-V Reference.pdfpages 1-2authoritative register saver classes and instruction semantics for add, sub, jal, jalr, sw, lw

Completion Check