驗(yàn)篇(lab4):RISC-V assembly (easy))
RISC-V assembly (easy)實(shí)驗(yàn)?zāi)繕?biāo)lab4 的第一關(guān)是純閱讀 問答不寫代碼目的是通過一份真實(shí)的反匯編call.asm把 RISC-V 的基礎(chǔ)機(jī)制摸透弄清楚參數(shù)寄存器、返回地址、棧幀在匯編層面長(zhǎng)什么樣。理解函數(shù)如何被調(diào)用與返回auipcjalr如何算出并跳到目標(biāo)地址。見識(shí)內(nèi)聯(lián) / 常量折疊對(duì)反匯編的影響以及大小端、未定義行為這些容易踩坑的細(xì)節(jié)。這一關(guān)是后面 Backtrace?;厮?、Alarm陷阱處理的底層地基——不把這些寄存器、指令、內(nèi)存布局搞熟后面寸步難行。前置知識(shí)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中生成可讀的匯編版本由編譯器/反匯編器自動(dòng)產(chǎn)生不是手寫的。后續(xù)所有分析都基于這個(gè)文件。2. RISC-V 調(diào)用約定重點(diǎn)要非常熟悉寄存器角色說明a0–a7參數(shù) / 返回值前 8 個(gè)整型/指針參數(shù)依次放a0..a7返回值也放a0ra返回地址調(diào)用指令自動(dòng)把下一條指令地址寫入rasp棧指針指向當(dāng)前棧頂s0/fp幀指針指向當(dāng)前棧幀基址便于訪問局部變量/保存的寄存器3. 兩個(gè)關(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)用。對(duì)比jal rd, imm目標(biāo)pc imm編碼在指令里編譯期確定的直接調(diào)用jalr目標(biāo)在寄存器里運(yùn)行時(shí)確定。4. 內(nèi)聯(lián) / 常量折疊解釋為什么 main 里找不到 f、g 的調(diào)用當(dāng)函數(shù)體很簡(jiǎn)單、且調(diào)用點(diǎn)參數(shù)是編譯期常量時(shí)編譯器會(huì)直接把結(jié)果算出來而不是真的去jalr調(diào)用。本題f(8)1就是典型8 是常量、g(x)x3也是常量表達(dá)式于是編譯器常量折疊出83112在main里直接li a1,12根本不會(huì)生成對(duì)f/g的調(diào)用指令。5. 大小端RISC-V 是小端little-endian一個(gè)多字節(jié)整數(shù)的低字節(jié)存放在低地址。這決定了int i 0x00646c72在內(nèi)存里的字節(jié)順序進(jìn)而決定%s打印出的字符串。實(shí)現(xiàn)思路本實(shí)驗(yà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)用。對(duì)照下方逐題解答核對(duì)。代碼實(shí)現(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 時(shí) 13 在哪個(gè)寄存器按 RISC-V 調(diào)用約定前 8 個(gè)整型參數(shù)放在a0–a7。看main調(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三個(gè)實(shí)參依次映射為a0格式串地址、a1f(8)112、a213。所以13 保存在a2寄存器。Q2main 中對(duì) f 的調(diào)用在哪里對(duì) g 的調(diào)用在哪里main的反匯編里沒有對(duì)f、g的jalr調(diào)用指令。原因在于編譯器做了內(nèi)聯(lián) 常量折疊f(8)中 8 是常量、g(x)x3也是常量表達(dá)式編譯器直接算出f(8)1 12在main里用li a1,12一步到位根本不會(huì)去調(diào)用f/g。注雖然f0xe與g0x0的標(biāo)簽和函數(shù)體仍保留在二進(jìn)制中各自就是一個(gè)a03的小函數(shù)但main并沒有跳轉(zhuǎn)到它們。所以調(diào)用點(diǎn)的答案是main 中沒有對(duì) f/g 的獨(dú)立調(diào)用結(jié)果已被內(nèi)聯(lián)/折疊為常數(shù) 12。Q3printf 函數(shù)位于哪個(gè)地址看 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。對(duì)應(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同時(shí)把pc4 0x34 4 0x38寫入ra作為 printf 返回后的續(xù)跑點(diǎn)。所以jalr 執(zhí)行后ra 0x38這是 printf 的返回地址而非 printf 的入口。Q5下列程序輸出是什么若為大端存儲(chǔ)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會(huì)按相反順序讀取要得到同樣的rld須把i改為0x726c6400而57616是作為%x的參數(shù)單獨(dú)傳入、與內(nèi)存布局無關(guān)因此不需要改變。Q6下列代碼 “y” 之后會(huì)打印什么為什么printf(x%d y%d,3);格式化串要求兩個(gè)%d卻只傳入了一個(gè)參數(shù)3 裝入a1對(duì)應(yīng)第一個(gè)%d的x。y對(duì)應(yīng)的第二個(gè)%d會(huì)去讀調(diào)用約定中本應(yīng)裝第二個(gè)參數(shù)的寄存器a2而a2里是本次調(diào)用前殘留的任意值。所以y之后打印的結(jié)果不確定取決于a2中之前保存的數(shù)據(jù)——這是典型的未定義行為UB也是為什么參數(shù)個(gè)數(shù)與格式串不匹配在 C 里是嚴(yán)重錯(cuò)誤。驗(yàn)證make fs.img后確認(rèn)user/call.asm已生成能獨(dú)立在call.asm中定位g/f/main/printf/exit的地址并解釋auipcjalr的計(jì)算過程逐題答案能自圓其說尤其 Q2 的內(nèi)聯(lián)、Q4 的返回地址0x38、Q5 的大小端。補(bǔ)充若把 Q5 那段代碼放進(jìn)一個(gè) xv6 用戶程序并make qemu運(yùn)行屏幕上確實(shí)會(huì)打印出He110 World可用來交叉驗(yàn)證你的推演。復(fù)盤本實(shí)驗(yàn)解決了什么表面是讀匯編答幾道題實(shí)則是為整個(gè) lab4 乃至 traps 機(jī)制做底層認(rèn)知鋪墊建立了C 語句 ? 機(jī)器指令的直覺——知道一個(gè)函數(shù)調(diào)用到底動(dòng)了哪些寄存器。吃透了a0(參數(shù)/返回值)、ra(返回地址)、sp/s0(棧幀) 三件套這正是后面Backtrace內(nèi)核?;厮菀闅v的東西。見識(shí)了編譯器優(yōu)化內(nèi)聯(lián)/常量折疊如何讓源碼里的函數(shù)調(diào)用在匯編里消失以及大小端、未定義行為這類隱蔽坑——后面調(diào)試內(nèi)核崩潰時(shí)這些問題會(huì)以更詭異的方式出現(xiàn)。為什么先學(xué)匯編再學(xué) trapstraps 的本質(zhì)是控制權(quán)在用戶態(tài)/內(nèi)核態(tài)之間、在指令邊界上轉(zhuǎn)移且必須完整保存/恢復(fù)現(xiàn)場(chǎng)。如果連普通函數(shù)調(diào)用怎么保存ra、怎么建棧幀、怎么用auipcjalr跳轉(zhuǎn)到目標(biāo)都不清楚就完全無法理解陷入內(nèi)核時(shí)為什么要先把sepc/ra/sp等搬進(jìn)trapframe。所以這一關(guān)是刻意前置的。強(qiáng)烈建議閱讀CSAPP第三章收獲函數(shù)調(diào)用約定能說清參數(shù)用寄存器傳RISC-V 的a0-a7而非全部壓棧以及返回值走a0注意 RISC-V schema、X86-64 schema 的區(qū)別。大小端endianness網(wǎng)絡(luò)協(xié)議、文件格式、跨平臺(tái)數(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)系動(dòng)態(tài)鏈接、函數(shù)指針。建議把前置知識(shí)里的寄存器約定表 auipc/jalr語義截圖存一份——Backtrace 實(shí)驗(yàn)里你會(huì)親手遍歷今天讀到的每一個(gè)棧幀。