ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MIT 6.S081 traps 实验篇(lab4):RISC-V assembly (easy)

MIT 6.S081 traps 实验篇(lab4):RISC-V assembly (easy) RISC-V assembly (easy)实验目标lab4 的第一关是纯阅读 问答不写代码目的是通过一份真实的反汇编call.asm把 RISC-V 的基础机制摸透弄清楚参数寄存器、返回地址、栈帧在汇编层面长什么样。理解函数如何被调用与返回auipcjalr如何算出并跳到目标地址。见识内联 / 常量折叠对反汇编的影响以及大小端、未定义行为这些容易踩坑的细节。这一关是后面 Backtrace栈回溯、Alarm陷阱处理的底层地基——不把这些寄存器、指令、内存布局搞熟后面寸步难行。前置知识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);}按题目要求执行makefs.img即可编译并在user/call.asm中生成可读的汇编版本由编译器/反汇编器自动产生不是手写的。后续所有分析都基于这个文件。2. RISC-V 调用约定重点要非常熟悉寄存器角色说明a0–a7参数 / 返回值前 8 个整型/指针参数依次放a0..a7返回值也放a0ra返回地址调用指令自动把下一条指令地址写入rasp栈指针指向当前栈顶s0/fp帧指针指向当前栈帧基址便于访问局部变量/保存的寄存器3. 两个关键指令auipc rd, immAdd Upper Immediate to PC。把 20 位立即数imm左移 12 位后加上当前pc结果存入rd。即rd pc (imm 12)。用于基于 PC 构造地址位置无关。jalr rd, offset(rs1)jump and link register。跳转到(rs1 offset)并把下一条指令地址(pc4) 存入rd通常rdra。用于间接/位置无关调用。对比jal rd, imm目标pc imm编码在指令里编译期确定的直接调用jalr目标在寄存器里运行时确定。4. 内联 / 常量折叠解释为什么 main 里找不到 f、g 的调用当函数体很简单、且调用点参数是编译期常量时编译器会直接把结果算出来而不是真的去jalr调用。本题f(8)1就是典型8 是常量、g(x)x3也是常量表达式于是编译器常量折叠出83112在main里直接li a1,12根本不会生成对f/g的调用指令。5. 大小端RISC-V 是小端little-endian一个多字节整数的低字节存放在低地址。这决定了int i 0x00646c72在内存里的字节顺序进而决定%s打印出的字符串。实现思路本实验不需要写代码核心是读 答。推荐流程make fs.img生成user/call.asm。在call.asm中搜g:、f:、main:、printf:、exit标签记下各自起始地址与指令。顺着main的指令流看参数如何装进a0/a1/a2、函数如何通过auipcjalr被调用。对照下方逐题解答核对。代码实现题目与标准解答原始材料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哪些寄存器保存函数参数main 调 printf 时 13 在哪个寄存器按 RISC-V 调用约定前 8 个整型参数放在a0–a7。看main调用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 的调用在哪里对 g 的调用在哪里main的反汇编里没有对f、g的jalr调用指令。原因在于编译器做了内联 常量折叠f(8)中 8 是常量、g(x)x3也是常量表达式编译器直接算出f(8)1 12在main里用li a1,12一步到位根本不会去调用f/g。注虽然f0xe与g0x0的标签和函数体仍保留在二进制中各自就是一个a03的小函数但main并没有跳转到它们。所以调用点的答案是main 中没有对 f/g 的独立调用结果已被内联/折叠为常数 12。Q3printf 函数位于哪个地址看 main 中调用 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注释也标明# 630 printf。所以printf 的地址是0x630。Q4main 中 printf 的 jalr 执行后ra 里是什么值回顾指令语义auipc rd, immrd pc (imm 12)。jalr rd, offset(rs1)跳到rs1 offset并把下一条指令地址 (pc4) 写入rd。对应到本题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 返回后的续跑点。所以jalr 执行后ra 0x38这是 printf 的返回地址而非 printf 的入口。Q5下列程序输出是什么若为大端存储i 该设成什么57616 要改吗unsignedinti0x00646c72;printf(H%x Wo%s,57616,i);57616 0xE110%x以小写十六进制打印 →e110。i 0x00646c72小端内存布局低字节在低地址为0x72(r)、0x6c(l)、0x64(d)、0x00(\0)。%s从i读到\0为止得到字符串rld。因此输出为He110 World注意%x是小写所以是e110而非E110。若 RISC-V 为大端内存高字节在低地址%s会按相反顺序读取要得到同样的rld须把i改为0x726c6400而57616是作为%x的参数单独传入、与内存布局无关因此不需要改变。Q6下列代码 “y” 之后会打印什么为什么printf(x%d y%d,3);格式化串要求两个%d却只传入了一个参数3 装入a1对应第一个%d的x。y对应的第二个%d会去读调用约定中本应装第二个参数的寄存器a2而a2里是本次调用前残留的任意值。所以y之后打印的结果不确定取决于a2中之前保存的数据——这是典型的未定义行为UB也是为什么参数个数与格式串不匹配在 C 里是严重错误。验证make fs.img后确认user/call.asm已生成能独立在call.asm中定位g/f/main/printf/exit的地址并解释auipcjalr的计算过程逐题答案能自圆其说尤其 Q2 的内联、Q4 的返回地址0x38、Q5 的大小端。补充若把 Q5 那段代码放进一个 xv6 用户程序并make qemu运行屏幕上确实会打印出He110 World可用来交叉验证你的推演。复盘本实验解决了什么表面是读汇编答几道题实则是为整个 lab4 乃至 traps 机制做底层认知铺垫建立了C 语句 ↔ 机器指令的直觉——知道一个函数调用到底动了哪些寄存器。吃透了a0(参数/返回值)、ra(返回地址)、sp/s0(栈帧) 三件套这正是后面Backtrace内核栈回溯要遍历的东西。见识了编译器优化内联/常量折叠如何让源码里的函数调用在汇编里消失以及大小端、未定义行为这类隐蔽坑——后面调试内核崩溃时这些问题会以更诡异的方式出现。为什么先学汇编再学 trapstraps 的本质是控制权在用户态/内核态之间、在指令边界上转移且必须完整保存/恢复现场。如果连普通函数调用怎么保存ra、怎么建栈帧、怎么用auipcjalr跳转到目标都不清楚就完全无法理解陷入内核时为什么要先把sepc/ra/sp等搬进trapframe。所以这一关是刻意前置的。强烈建议阅读CSAPP第三章收获函数调用约定能说清参数用寄存器传RISC-V 的a0-a7而非全部压栈以及返回值走a0注意 RISC-V schema、X86-64 schema 的区别。大小端endianness网络协议、文件格式、跨平台数据交换都绕不开能用0x00646c72在大小端下的内存布局举例说明是基本功。未定义行为UB如 Q6 的参数不匹配能联系到为什么编译器可以做激进优化“为什么 UB 不能依赖”。jalvsjalr/ 位置无关代码PIC能区分直接调用与间接调用并联系动态链接、函数指针。建议把前置知识里的寄存器约定表 auipc/jalr语义截图存一份——Backtrace 实验里你会亲手遍历今天读到的每一个栈帧。
返回列表