news 2026/9/27 10:50:49

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MIT 6.S081 traps 实验篇(lab4):RISC-V assembly (easy)

RISC-V assembly (easy)


实验目标

lab4 的第一关是纯阅读 + 问答,不写代码,目的是通过一份真实的反汇编(call.asm)把 RISC-V 的基础机制摸透:

  1. 弄清楚参数寄存器、返回地址、栈帧在汇编层面长什么样。
  2. 理解函数如何被调用与返回(auipc+jalr如何算出并跳到目标地址)。
  3. 见识内联 / 常量折叠对反汇编的影响,以及大小端、未定义行为这些容易踩坑的细节。

这一关是后面 Backtrace(栈回溯)、Alarm(陷阱处理)的底层地基——不把这些寄存器、指令、内存布局搞熟,后面寸步难行。


前置知识

1. 如何生成 call.asm

仓库里已有user/call.c:

/* * user/call.c */#include"kernel/param.h"#include"kernel/types.h"#include"kernel/stat.h"#include"user/user.h"intg(intx){returnx+3;}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;返回值也放a0
ra返回地址调用指令自动把"下一条指令地址"写入ra
sp栈指针指向当前栈顶
s0/fp帧指针指向当前栈帧基址,便于访问局部变量/保存的寄存器

3. 两个关键指令

4. 内联 / 常量折叠(解释"为什么 main 里找不到 f、g 的调用")

当函数体很简单、且调用点参数是编译期常量时,编译器会直接把结果算出来,而不是真的去jalr调用。本题f(8)+1就是典型:8 是常量、g(x)=x+3也是常量表达式,于是编译器常量折叠出8+3+1=12,在main里直接li a1,12,根本不会生成对f/g的调用指令。

5. 大小端

RISC-V 是小端(little-endian):一个多字节整数的低字节存放在低地址。这决定了int i = 0x00646c72在内存里的字节顺序,进而决定%s打印出的字符串。


实现思路

本实验不需要写代码,核心是"读 + 答"。推荐流程:

  1. make fs.img生成user/call.asm。
  2. 在call.asm中搜<g>:、<f>:、<main>:、<printf>:、<exit>标签,记下各自起始地址与指令。
  3. 顺着main的指令流,看参数如何装进a0/a1/a2、函数如何通过auipc+jalr被调用。
  4. 对照下方逐题解答核对。

代码实现(题目与标准解答)

原始材料: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 x+3; } 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 <malloc+0xea> 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 <malloc+0xea> 30: 00000097 auipc ra,0x0 34: 600080e7 jalr 1536(ra) # 630 <printf>

三个实参依次映射为:a0=格式串地址、a1=f(8)+1=12、a2=13。所以13 保存在a2寄存器。

Q2:main 中对 f 的调用在哪里?对 g 的调用在哪里?

main的反汇编里没有对f、g的jalr调用指令。原因在于编译器做了内联 + 常量折叠:f(8)中 8 是常量、g(x)=x+3也是常量表达式,编译器直接算出f(8)+1 = 12,在main里用li a1,12一步到位,根本不会去调用f/g。

注:虽然<f>(0xe)与<g>(0x0)的标签和函数体仍保留在二进制中(各自就是一个a0+3的小函数),但main并没有跳转到它们。所以"调用点"的答案是:main 中没有对 f/g 的独立调用,结果已被内联/折叠为常数 12。

Q3:printf 函数位于哪个地址?

看 main 中调用 printf 的两行:

30: 00000097 auipc ra,0x0 34: 600080e7 jalr 1536(ra) # 630 <printf>

auipc ra,0x0把pc(0x30) + 0 = 0x30存入ra;jalr 1536(ra)跳到ra + 1536(0x600) = 0x630,注释也标明# 630 <printf>。所以printf 的地址是0x630。

Q4:main 中 printf 的 jalr 执行后,ra 里是什么值?

回顾指令语义:

对应到本题:

30: 00000097 auipc ra,0x0 # ra = 0x30 + 0 = 0x30 34: 600080e7 jalr 1536(ra) # 630 <printf>

所以jalr 执行后ra = 0x38(这是 printf 的返回地址,而非 printf 的入口)。

Q5:下列程序输出是什么?若为大端存储,i 该设成什么?57616 要改吗?

unsignedinti=0x00646c72;printf("H%x Wo%s",57616,&i);

因此输出为He110 World(注意%x是小写,所以是e110而非E110)。

Q6:下列代码 “y=” 之后会打印什么?为什么?

printf("x=%d y=%d",3);

格式化串要求两个%d,却只传入了一个参数(3 装入a1,对应第一个%d的x)。y对应的第二个%d会去读调用约定中本应装第二个参数的寄存器a2,而a2里是本次调用前残留的任意值。

所以"y="之后打印的结果不确定,取决于a2中之前保存的数据——这是典型的未定义行为(UB),也是为什么"参数个数与格式串不匹配"在 C 里是严重错误。


验证

  1. make fs.img后确认user/call.asm已生成;
  2. 能独立在call.asm中定位g/f/main/printf/exit的地址,并解释auipc+jalr的计算过程;
  3. 逐题答案能自圆其说(尤其 Q2 的内联、Q4 的返回地址0x38、Q5 的大小端)。

补充:若把 Q5 那段代码放进一个 xv6 用户程序并make qemu运行,屏幕上确实会打印出He110 World,可用来交叉验证你的推演。


复盘

本实验解决了什么

表面是"读汇编答几道题",实则是为整个 lab4 乃至 traps 机制做底层认知铺垫:

  1. 建立了"C 语句 ↔ 机器指令"的直觉——知道一个函数调用到底动了哪些寄存器。
  2. 吃透了a0(参数/返回值)、ra(返回地址)、sp/s0(栈帧) 三件套,这正是后面Backtrace(内核栈回溯)要遍历的东西。
  3. 见识了编译器优化(内联/常量折叠)如何让"源码里的函数调用"在汇编里消失,以及大小端、未定义行为这类隐蔽坑——后面调试内核崩溃时,这些问题会以更诡异的方式出现。

为什么先学汇编再学 traps

traps 的本质是"控制权在用户态/内核态之间、在指令边界上转移,且必须完整保存/恢复现场"。如果连"普通函数调用怎么保存ra、怎么建栈帧、怎么用auipc+jalr跳转到目标"都不清楚,就完全无法理解"陷入内核时为什么要先把sepc/ra/sp等搬进trapframe"。所以这一关是刻意前置的。

强烈建议阅读CSAPP第三章

收获

建议把"前置知识"里的寄存器约定表 +auipc/jalr语义截图存一份——Backtrace 实验里你会亲手遍历今天读到的每一个栈帧。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 10:47:12

Cortex-M FPU中断嵌套HardFault排查:惰性保存与优先级配置

1. 从一次HardFault定位说起&#xff1a;FPU上下文与中断嵌套的隐秘冲突那个HardFault出现在凌晨两点。设备跑的是Cortex-M4内核&#xff0c;带FPU&#xff0c;FreeRTOS上跑着几个任务&#xff0c;串口每隔几秒打印一次姿态数据。现象很诡异&#xff1a;平时跑几个小时都没事&a…

作者头像 李华
网站建设 2026/9/27 10:42:30

单片机C++实战:零开销抽象与工程化封装指南

1. 从裸机到C&#xff1a;为什么要在单片机上折腾这门语言很多人第一次接触单片机编程&#xff0c;都是从C语言开始的。51单片机、STM32、GD32、ESP32&#xff0c;这些芯片的官方例程、教学视频、开源项目&#xff0c;清一色都是C语言。江科大的51和32笔记在网上流传甚广&#…

作者头像 李华
网站建设 2026/9/27 10:42:22

嵌入式烧录版本管理:固件可追溯、可复现的工程实践

1. 为什么“烧录程序版本管理”是芯片开发里最危险的隐形雷区你有没有遇到过这样的场景&#xff1a;凌晨两点&#xff0c;产线突然停摆&#xff0c;几十台设备集体变砖&#xff1b;或者客户反馈新固件功能异常&#xff0c;回溯发现测试用的是三个月前的旧版本&#xff1b;又或者…

作者头像 李华
网站建设 2026/9/27 10:40:51

基于STM32的多传感器实验室消防预警系统设计与实现

1. 为什么我要用STM32做一套实验室消防预警系统实验室这个场景&#xff0c;跟普通的办公室、住宅有本质区别。普通场所着火&#xff0c;大概率是电线老化或者明火引燃&#xff1b;实验室里可能同时存在酒精灯、乙醚、氢气钢瓶、锂电池充放电测试台&#xff0c;甚至还有学生半夜…

作者头像 李华