:RISC-V assembly (easy))
RISC-V assembly (easy)實驗?zāi)繕?biāo)lab4 的第一關(guān)是純閱讀 問答不寫代碼目的是通過一份真實的反匯編call.asm把 RISC-V 的基礎(chǔ)機制摸透弄清楚參數(shù)寄存器、返回地址、棧幀在匯編層面長什么樣。理解函數(shù)如何被調(diào)用與返回auipcjalr如何算出并跳到目標(biāo)地址。見識內(nèi)聯(lián) / 常量折疊對反匯編的影響以及大小端、未定義行為這些容易踩坑的細(xì)節(jié)。這一關(guān)是后面 Backtrace?;厮?、Alarm陷阱處理的底層地基——不把這些寄存器、指令、內(nèi)存布局搞熟后面寸步難行。前置知識1. 如何生成 call.asm倉庫里已有user/call.c/* * user/call.c */#includekernel/param.h#includekernel/types.h#includekernel/stat.h#includeuser/user.hintg(intx){returnx3;}intf(intx){returng(x);}voidmain(void){printf(%d %d\n,f(8)1,13);exit(0);}按題目要求執(zhí)行makefs.img即可編譯并在user/call.asm中生成可讀的匯編版本由編譯器/反匯編器自動產(chǎn)生不是手寫的。后續(xù)所有分析都基于這個文件。2. RISC-V 調(diào)用約定重點要非常熟悉寄存器角色說明a0–a7參數(shù) / 返回值前 8 個整型/指針參數(shù)依次放a0..a7返回值也放a0ra返回地址調(diào)用指令自動把下一條指令地址寫入rasp棧指針指向當(dāng)前棧頂s0/fp幀指針指向當(dāng)前棧幀基址便于訪問局部變量/保存的寄存器3. 兩個關(guān)鍵指令auipc rd, immAdd Upper Immediate to PC。把 20 位立即數(shù)imm左移 12 位后加上當(dāng)前pc結(jié)果存入rd。即rd pc (imm 12)。用于基于 PC 構(gòu)造地址位置無關(guān)。jalr rd, offset(rs1)jump and link register。跳轉(zhuǎn)到(rs1 offset)并把下一條指令地址(pc4) 存入rd通常rdra。用于間接/位置無關(guān)調(diào)用。對比jal rd, imm目標(biāo)pc imm編碼在指令里編譯期確定的直接調(diào)用jalr目標(biāo)在寄存器里運行時確定。4. 內(nèi)聯(lián) / 常量折疊解釋為什么 main 里找不到 f、g 的調(diào)用當(dāng)函數(shù)體很簡單、且調(diào)用點參數(shù)是編譯期常量時編譯器會直接把結(jié)果算出來而不是真的去jalr調(diào)用。本題f(8)1就是典型8 是常量、g(x)x3也是常量表達(dá)式于是編譯器常量折疊出83112在main里直接li a1,12根本不會生成對f/g的調(diào)用指令。5. 大小端RISC-V 是小端little-endian一個多字節(jié)整數(shù)的低字節(jié)存放在低地址。這決定了int i 0x00646c72在內(nèi)存里的字節(jié)順序進(jìn)而決定%s打印出的字符串。實現(xiàn)思路本實驗不需要寫代碼核心是讀 答。推薦流程make fs.img生成user/call.asm。在call.asm中搜g:、f:、main:、printf:、exit標(biāo)簽記下各自起始地址與指令。順著main的指令流看參數(shù)如何裝進(jìn)a0/a1/a2、函數(shù)如何通過auipcjalr被調(diào)用。對照下方逐題解答核對。代碼實現(xiàn)題目與標(biāo)準(zhǔn)解答原始材料call.asm/* * user/call.asm */ user/_call: file format elf64-littleriscv Disassembly of section .text: 0000000000000000 g: #include kernel/param.h #include kernel/types.h #include kernel/stat.h #include user/user.h int g(int x) { 0: 1141 addi sp,sp,-16 2: e422 sd s0,8(sp) 4: 0800 addi s0,sp,16 return x3; } 6: 250d addiw a0,a0,3 8: 6422 ld s0,8(sp) a: 0141 addi sp,sp,16 c: 8082 ret 000000000000000e f: int f(int x) { e: 1141 addi sp,sp,-16 10: e422 sd s0,8(sp) 12: 0800 addi s0,sp,16 return g(x); } 14: 250d addiw a0,a0,3 16: 6422 ld s0,8(sp) 18: 0141 addi sp,sp,16 1a: 8082 ret 000000000000001c main: void main(void) { 1c: 1141 addi sp,sp,-16 1e: e406 sd ra,8(sp) 20: e022 sd s0,0(sp) 22: 0800 addi s0,sp,16 printf(%d %d\n, f(8)1, 13); 24: 4635 li a2,13 26: 45b1 li a1,12 28: 00000517 auipc a0,0x0 2c: 7b050513 addi a0,a0,1968 # 7d8 malloc0xea 30: 00000097 auipc ra,0x0 34: 600080e7 jalr 1536(ra) # 630 printf exit(0); 38: 4501 li a0,0 3a: 00000097 auipc ra,0x0 3e: 27e080e7 jalr 638(ra) # 2b8 exit ...Q1哪些寄存器保存函數(shù)參數(shù)main 調(diào) printf 時 13 在哪個寄存器按 RISC-V 調(diào)用約定前 8 個整型參數(shù)放在a0–a7??磎ain調(diào)用printf的匯編printf(%d %d\n, f(8)1, 13); 24: 4635 li a2,13 26: 45b1 li a1,12 28: 00000517 auipc a0,0x0 2c: 7b050513 addi a0,a0,1968 # 7d8 malloc0xea 30: 00000097 auipc ra,0x0 34: 600080e7 jalr 1536(ra) # 630 printf三個實參依次映射為a0格式串地址、a1f(8)112、a213。所以13 保存在a2寄存器。Q2main 中對 f 的調(diào)用在哪里對 g 的調(diào)用在哪里main的反匯編里沒有對f、g的jalr調(diào)用指令。原因在于編譯器做了內(nèi)聯(lián) 常量折疊f(8)中 8 是常量、g(x)x3也是常量表達(dá)式編譯器直接算出f(8)1 12在main里用li a1,12一步到位根本不會去調(diào)用f/g。注雖然f0xe與g0x0的標(biāo)簽和函數(shù)體仍保留在二進(jìn)制中各自就是一個a03的小函數(shù)但main并沒有跳轉(zhuǎn)到它們。所以調(diào)用點的答案是main 中沒有對 f/g 的獨立調(diào)用結(jié)果已被內(nèi)聯(lián)/折疊為常數(shù) 12。Q3printf 函數(shù)位于哪個地址看 main 中調(diào)用 printf 的兩行30: 00000097 auipc ra,0x0 34: 600080e7 jalr 1536(ra) # 630 printfauipc ra,0x0把pc(0x30) 0 0x30存入rajalr 1536(ra)跳到ra 1536(0x600) 0x630注釋也標(biāo)明# 630 printf。所以printf 的地址是0x630。Q4main 中 printf 的 jalr 執(zhí)行后ra 里是什么值回顧指令語義auipc rd, immrd pc (imm 12)。jalr rd, offset(rs1)跳到rs1 offset并把下一條指令地址 (pc4) 寫入rd。對應(yīng)到本題30: 00000097 auipc ra,0x0 # ra 0x30 0 0x30 34: 600080e7 jalr 1536(ra) # 630 printf第一行imm0ra 0x30 (012) 0x30。第二行jalr跳到ra(0x30) 0x600 0x630即 printf同時把pc4 0x34 4 0x38寫入ra作為 printf 返回后的續(xù)跑點。所以jalr 執(zhí)行后ra 0x38這是 printf 的返回地址而非 printf 的入口。Q5下列程序輸出是什么若為大端存儲i 該設(shè)成什么57616 要改嗎unsignedinti0x00646c72;printf(H%x Wo%s,57616,i);57616 0xE110%x以小寫十六進(jìn)制打印 →e110。i 0x00646c72小端內(nèi)存布局低字節(jié)在低地址為0x72(r)、0x6c(l)、0x64(d)、0x00(\0)。%s從i讀到\0為止得到字符串rld。因此輸出為He110 World注意%x是小寫所以是e110而非E110。若 RISC-V 為大端內(nèi)存高字節(jié)在低地址%s會按相反順序讀取要得到同樣的rld須把i改為0x726c6400而57616是作為%x的參數(shù)單獨傳入、與內(nèi)存布局無關(guān)因此不需要改變。Q6下列代碼 “y” 之后會打印什么為什么printf(x%d y%d,3);格式化串要求兩個%d卻只傳入了一個參數(shù)3 裝入a1對應(yīng)第一個%d的x。y對應(yīng)的第二個%d會去讀調(diào)用約定中本應(yīng)裝第二個參數(shù)的寄存器a2而a2里是本次調(diào)用前殘留的任意值。所以y之后打印的結(jié)果不確定取決于a2中之前保存的數(shù)據(jù)——這是典型的未定義行為UB也是為什么參數(shù)個數(shù)與格式串不匹配在 C 里是嚴(yán)重錯誤。驗證make fs.img后確認(rèn)user/call.asm已生成能獨立在call.asm中定位g/f/main/printf/exit的地址并解釋auipcjalr的計算過程逐題答案能自圓其說尤其 Q2 的內(nèi)聯(lián)、Q4 的返回地址0x38、Q5 的大小端。補充若把 Q5 那段代碼放進(jìn)一個 xv6 用戶程序并make qemu運行屏幕上確實會打印出He110 World可用來交叉驗證你的推演。復(fù)盤本實驗解決了什么表面是讀匯編答幾道題實則是為整個 lab4 乃至 traps 機制做底層認(rèn)知鋪墊建立了C 語句 ? 機器指令的直覺——知道一個函數(shù)調(diào)用到底動了哪些寄存器。吃透了a0(參數(shù)/返回值)、ra(返回地址)、sp/s0(棧幀) 三件套這正是后面Backtrace內(nèi)核?;厮菀闅v的東西。見識了編譯器優(yōu)化內(nèi)聯(lián)/常量折疊如何讓源碼里的函數(shù)調(diào)用在匯編里消失以及大小端、未定義行為這類隱蔽坑——后面調(diào)試內(nèi)核崩潰時這些問題會以更詭異的方式出現(xiàn)。為什么先學(xué)匯編再學(xué) trapstraps 的本質(zhì)是控制權(quán)在用戶態(tài)/內(nèi)核態(tài)之間、在指令邊界上轉(zhuǎn)移且必須完整保存/恢復(fù)現(xiàn)場。如果連普通函數(shù)調(diào)用怎么保存ra、怎么建棧幀、怎么用auipcjalr跳轉(zhuǎn)到目標(biāo)都不清楚就完全無法理解陷入內(nèi)核時為什么要先把sepc/ra/sp等搬進(jìn)trapframe。所以這一關(guān)是刻意前置的。強烈建議閱讀CSAPP第三章收獲函數(shù)調(diào)用約定能說清參數(shù)用寄存器傳RISC-V 的a0-a7而非全部壓棧以及返回值走a0注意 RISC-V schema、X86-64 schema 的區(qū)別。大小端endianness網(wǎng)絡(luò)協(xié)議、文件格式、跨平臺數(shù)據(jù)交換都繞不開能用0x00646c72在大小端下的內(nèi)存布局舉例說明是基本功。未定義行為UB如 Q6 的參數(shù)不匹配能聯(lián)系到為什么編譯器可以做激進(jìn)優(yōu)化“為什么 UB 不能依賴”。jalvsjalr/ 位置無關(guān)代碼PIC能區(qū)分直接調(diào)用與間接調(diào)用并聯(lián)系動態(tài)鏈接、函數(shù)指針。建議把前置知識里的寄存器約定表 auipc/jalr語義截圖存一份——Backtrace 實驗里你會親手遍歷今天讀到的每一個棧幀。