mit6.828 - lab1笔记

news/2024/10/13 16:20:55

安装环境编译qemu

image.png

1. PC启动

打开两个窗口,在第一个窗口中 make qemu-gdb,会启动内核,但在执行第一个指令之前停下;
在第二个窗口中make gdb,实时观察第一个窗口中的执行过程。

image.png

从这里可以观察到:

  • IBM PC 在物理地址 0x000ffff0 开始执行, 位于为 ROM BIOS 保留的 64KB 区域的最顶部。
  • PC 的第一个指令执行的是 CS=0xf000 IP=0xfff0
  • 第一条指令是 jmp 指令, 跳转到分段地址 CS = 0xf000 和 IP = 0xe05b。

image.png

## 为什么第一个指令在这个位置?
这是因为 8088的BIOS 是“硬连线”的 到物理地址范围 0x000f0000-0x000fffff, 从而确保BIOS首先获得对机器的控制
0xffff0 是 BIOS 结束前的 16 个字节 (0x100000),BIOS做的第一件事是向后jmp 到 BIOS 中较早的位置;

2. bootloader

bootloader 的开始
bootsec 如果磁盘是可启动的, 第一个扇区称为 boot sector, 因为这是引导加载程序代码所在的位置。

当 BIOS 找到可启动软盘或硬盘时, 会将其加载(512字节)至物理地址的内存的0x7c00 0x7dff。然后64KB大小的BOIS的最后一句话即是:
jmp $0x0000,$0x7c00
将控制转交给了 bootloader

image.png

boot loader 的任务有两个:

  1. 将处理器从实模式切换到保护模式。因为实模式最多只能访问1MB的内存。
  2. 从硬盘读取内核,加载到内存。bootstrap使用特殊I/O指令,直接访问IDE磁盘设备存储器来读取。

boot loader 的实现:
 一个汇编语言源文件,boot/boot.S
 一个 C 源文件 boot/main.c
 反汇编文件: obj/boot/boot.asm

先看代码、然后看反汇编、再调试,摸清楚 boot loader 的流程

阅读源码

boot/boot.S的内容:

  1. 加载全局描述符表 GDT
  2. 开启保护模式:将CR0寄存器的PE_ON位置1
  3. 通过ljmp进入保护模式
  4. 加载各个段描述符
  5. 跳转至 bootmain.c

boot/bootmain.c的内容

  1. 加载kernel的elf文件头:从硬盘1号扇区(第二个扇区)的起始处读取4KB大小的内容至 0x0010_0000处,并将其视为ELF结构体
  2. 将 kernel 的各个段加载至内存

boot/boot.S

boot.S 中有一个令人迷惑的代码:

image.png

在即将跳转到C语言实现的bootmain的时候,居然将 start标号 给了esp,那么 start 代表了什么?

image.png

啊,start位于代码的一开始的地方,这里不是应该存代码吗?给了esp,后面栈不得把这下面的代码的都给覆盖了?
稍等下,栈是从高地址向低地址生长的,这里boot.S的代码在ide里看虽然写在start下面,但是在内存里是start更高的地方。从 obj/boot.asm 里来看:

image.png

start 位于 0x7C00,之后的代码位于0x7C00之上,而栈则向0x7C00下方生长

image.png

boot/main.c

boot/bootmain.c的内容

  1. 加载kernel的elf文件头:从硬盘1号扇区(第二个扇区)的起始处读取4KB大小的内容至 0x0010_0000处,并将其视为ELF结构体
  2. 将 kernel 的各个段加载至内存

image.png

其中的循环会逐个将 /obj/kern/kernl 的段加载至对应的物理地址(注意,readseg 的第一个参数是 ph->p_pa),可以通过 objdump -l kernel 查看:

image.png

最终内存视图如下:

image.png


##### 看反汇编发现了一些有趣的事情:
1. 循环中,调用函数后的递增操作,在汇编层面会在调用之前发生![image.png](https://pic-bed-1258913394.cos.ap-nanjing.myqcloud.com/20240501213701.png)2. 调用前,调用者负责传参,被调者负责保护现场,还原现场;返回后,调用者负责将传参占用的空间还原

关于ELF和编译链接

在开发者完成一个C语言程序程序 xxx.c ,为了让他跑起来,需要由编译器将其编译成 xxx.o 的对象文件,然后由链接器将所有已经编译的对象文件链接成 xxx 可执行文件。


3. 内核

目的:理解lab1的简易内核的工作过程

任务:阅读 /kern 下的代码。

lab1的内核功能十分简单,如上文中运行起来的那样,他的shell只提供两个功能,help和kerninfo。
内核相关的代码位于 /kern 之下。

entry.S:初始化内存映射,设置页表、栈指针
entrypgdir.c:页表设计init.c:初始化shell,初始化终端设备、启动shell
console.h, console.c:终端功能的实现
printf.c:打印功能的实现
monitor.h, monitor.c:shell功能的实现

挺好,为了理解 lab1 的内核,接下来就沿着 entry.S 和 init.c 去分析内核。
即,分析entry.S对内存映射的处理、init.c 中终端设备的初始化shell的处理

内存映射的处理

关于内存的处理,lab1目前没有内存管理,只是用起来了虚拟内存,将4MB物理内存映射到原位和高处。即:

  • 0x00000000 至 0x00400000 的物理地址 -> 0x00000000 至 0x00400000 的虚拟地址
  • 0x00000000 至 0x00400000 的物理地址 -> 0xf0000000 至 0xf0400000 的虚拟地址
    毕竟这么大的内存已经足够映射当前内核了。

先来看看怎么映射的

entry.S:加载页表

在 boolloader 阶段,bootmain 最后通过 ((void (*)(void)) (ELFHDR->e_entry))();
将控制转交给了 /kern/entry.S,然后来看看entry.S

image.png

关于数组 entry_pgdir

entry.S 首先读取了页表 entry_pgdir,这个变量在 /kern/entrypgdir.c 中定义:

pte_t entry_pgtable[NPTENTRIES];__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {// Map VA's [0, 4MB) to PA's [0, 4MB)[0]= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)[KERNBASE>>PDXSHIFT]= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};__attribute__((__aligned__(PGSIZE)))
pte_t entry_pgtable[NPTENTRIES] = {0x000000 | PTE_P | PTE_W,0x001000 | PTE_P | PTE_W,0x002000 | PTE_P | PTE_W,0x003000 | PTE_P | PTE_W,0x004000 | PTE_P | PTE_W,0x005000 | PTE_P | PTE_W,0x006000 | PTE_P | PTE_W,0x007000 | PTE_P | PTE_W,0x008000 | PTE_P | PTE_W,0x009000 | PTE_P | PTE_W,0x00a000 | PTE_P | PTE_W,//省略...
}

其中 [0] = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P, 实现了
0x00000000 至 0x00400000 的物理地址 -> 0x00000000 至 0x00400000 的虚拟地址
[KERNBASE>>PDXSHIFT] = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W 实现了
0x00000000 至 0x00400000 的物理地址 -> 0xf0000000 至 0xf0400000 的虚拟地址

关于页表的映射和计算方法,见另一个单独的笔记 "lab1 关于页表的知识"

关于宏 RELOC

从代码中可以看到,在页表加载之前,所有的符号都需要使用宏 RELOC ,其含义是将符号的地址减去 0xF000_0000,即,将虚拟地址转化为真实的物理地址。
这就说明 entry.S 被链接到了 0xF000_0000 上。
通过 objdump -h 来看也确实如此

image.png

但是对应的makefile是将其指定到 0xf000_0000 上的,可以从 /kern/kernel.ld 中找到

image.png

关于 bootstack

把目光回到 entry.S 的代码,在代码的最后通过标号 bootstack 和 bootstacktop定义了栈的位置,话说,这里究竟对应的物理地址是哪里呢?

image.png

可以看到 bootstack 紧邻 .data 段
通过 readelf -s kernel 查看

image.png

结合 objdump -h kernel

image.png

确实如此,bootstack 和 .data都位于 0xf010_8000 ,那么物理地址就是 0x0010_8000
栈顶 bootstacktop 的物理地址则是 0x0011_0000
在内存里看呢?

image.png


init.c:内核初始化

init.c 中最核心的函数是 i386_init

image.png

关于 清空BSS段

edata[]end[] 是在哪里定义的?这两个变量看起来指的是bss段的开始和结束。
这种问题当然要去看链接脚本了,查看 kern/kernel.ld

image.png

显示输出的处理

这里涉及的代码有

kern:console.h, console.c :涉及终端设备的初始化printf.c :涉及printf的实现
lib:printfmt.c:支撑printf的实现readline.c:实现从终端读取string.c:涉及字符串的处理,支撑printf的实现
inc:string.h:涉及字符串的处理,支撑printf的实现
关于 cons_init

这里主要用于初始化终端显示器的硬件设置,其中代码使用汇编,通过in out指令与设备交互,不过多深究了。
image.png

关于 printf 的实现

printf 的实现这里借大佬的说明图示意:

image.png

往控制台写字符串,本质还是往物理地址0xB8000开始的显存写数据

jos 的练习提到 printf 的实现需要补充,具体位于 /lib/printfmt.c : vprintfmt 中

image.png

image.png

shell的处理

这里涉及的代码有

kern:monitor.h, monitor.c :命令的解析、各种命令的实现
关于monitor的实现

先看看 monitor.h

image.png

然后看看 monitor.c

image.png

这么看,只要在 commands[] 中填充 backtrace 的数据就可以补充这个功能了。

image.png

monitor 是怎么实现的呢?,比较短,直接放代码了

void
monitor(struct Trapframe *tf)
{char *buf;cprintf("Welcome to the JOS kernel monitor!\n");cprintf("Type 'help' for a list of commands.\n");while (1) {buf = readline("K> ");if (buf != NULL)if (runcmd(buf, tf) < 0)break;}
}

本质就是一个循环,打印出 K> 然后接受输入,然后根据输入执行命令。看起来就像是大一C语言课设的XXX管理系统一样。
看看 runcmd 如何实现:

image.png

挺好,那么现在我们要做的就是实现 backtrace。

堆栈

涉及到的代码:

kern:kdebug.h、kdebug.c:涉及Eipdebuginfo和debuginfo_eip的实现
inc:stab.h:涉及Stab表的数据结构x86.h:涉及读取寄存器的内敛汇编

这里我们回归到jos的学习任务,研究关于栈帧的处理。并补充一些函数:
/kern/monitor.c:mon_backtrace
/kern/kdebug.c:debuginfo_eip、stab_binsearch

关于backtrace的实现

关于栈帧
栈帧,就是调用函数的时候,处理形参传递和实参存储的数据结构。
在调用函数时,调用者负责传递形参,被调者负责保护现场、恢复现场,最后调用者将形参释放掉。
这之中需要调用者和被调者的约定:
比如 函数列表中的参数,是从右至左的顺序入栈的之类的。

这里继续借用大佬 gatsby123 博客中的图,简单示意,不做深究

image.png

jos的练习11 让我们完成 mon_backtrace,希望我们将每个栈帧按照这样的格式输出:

Stack backtrace:ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061...

不过好在 jos 已经实现了一些函数,供我们调用了,位于 /inc/x86.h
这里提供了一些内联汇编,用于读取各种寄存器的值

image.png

完成这一步也是很简单啦

image.png

但是 jos 的练习12上了强度,让我们打印出这样的效果:

image.png

就是在上面的基础上,显示当前栈帧所在的文件和,以及调用在文件的所在函数的第几行发生。
为了实现这一功能,jos 在kern/kdebug.h 和 kern/kdebug.c 中提供了支持:

image.png

可以看到 Eipdebuginfo 用于存储当前eip的相关信息。这种功能的背后当然需要编译器的支持,为了方便debug,编译器可以通过stab将这些信息保存下来,

关于stab

按照 exercise12 的提示,通过 kernel.ld 可以看到.stab和 .stabstr 的相关连接选项

image.png

可以看到其中定义了 __STAB_BEGIN__ __STAB__END__ __STABSTR_BEGIN__ __STABSTR_END__

通过 objdump -h obj/kern/kernel 可以看到 stab 表

image.png

通过 objdump -G obj/kern/kernel 可以看到stab的内容

image.png

其中包含1213项,每项包括

symnum:序号
n_type:类型
n_othor:杂项信息
n_desc:描述信息
n_value:表示地址。特别要注意的是,这里只有FUN类型的符号的地址是绝对地址,SLINE符号的地址是偏移量,
n_strx:stabstr表中对应的字符串的序号
string:stabstr表中对应的字符串

在 stab.h中有对应的数据结构:

image.png
那么这些信息要怎么使用呢,看看kdebug.c


stab_binsearch(stabs, region_left, region_right, type, addr)某些符号表项类型按指令地址递增顺序排列。 例如,标记函数的 N_FUN 符号表项(n_type == N_FUN 的符号表项)和标记源文件的 N_SO 符号表项。给定指令地址后,该函数会查找包含该地址的 "type "类型的符号表项。搜索范围为[*region_left, *region_right]。因此,要搜索一整套 N 个符号表项,可以执行以下操作// left = 0;
// right = N - 1; /* 最右边的符号表项 */
// stab_binsearch(stabs, &left, &right, type, addr);搜索会修改 *region_left 和 *region_right 以括住 "addr"。 *region_left 指向包含'addr'的匹配符号表项,*region_right 指向下一个符号表项之前。 如果 *region_left > *region_right,则表示 "addr "不包含在任何匹配的符号表项中。// 例如,给定这些 N_SO 符号表项:
// 索引类型 地址
// 0 SO f0100000
// 13 SO f0100040
// 117 SO f0100176
// 118 SO f0100178
// 555 SO f0100652
// 556 SO f0100654
// 657 SO f0100849
// 此代码:
// left = 0, right = 657;
// stab_binsearch(stabs, &left, &right, N_SO, 0xf0100184);
// 将退出设置 left = 118, right = 554.

这里给出了 stab_binsearch 的使用说明,从函数名可以看出来他是使用二分查找算法从stab中查找addr指定的type类型的符号,然后通过left返回出来。来简单看看代码:

image.png

然后来看看要处理的 debuginfo_eip

image.png

image.png

到现在为止,已经找到了所在文件名、所在函数名、所在函数地址、所在函数名长度、相对函数的偏移
就差所在行号了,找行号的代码很好写啊,照着写就行了,这个函数调用,将范围改一下,然后类型改成代码段的行就行了,因为eip只会在代码段里移动。

stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);

但是,行号究竟是stab中的哪个成员提供的啊?
image.png

观察一波 objdump -G 的输出

image.png

目测 n_value对应的是SLINE的内存地址,而n_desc看着更像行号一些,于是:

image.png

补充一下 monitor.c

image.png

编译测试一下:

image.png

看着好像成功了,试试评分

image.png

收工。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/26949.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

混入、插件、插槽、vuex、本地存储

【混入】# mixin(混入)功能:可以把多个组件共用的配置提取成一个混入对象,不需要在每个组件中都写了 使用步骤 。 。 。 【插件】1 # 1 写plugins/index.js2 import Vue from "vue";3 import axios from "axios";4 import hunru from "@/mixin&quo…

Linux进程

程序与进程 程序:是可执行文件,其本质是是一个文件,程序是静态的,同一个程序可以运行多次,产生多个进程 进程:它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,进程是动态的,进程的生命周期是从程序运行到程序退出 父子进程:当一个进程A…

RISC-V SoC研发flow的总结

RISC-V SoC研发flow的总结 今年的流片接近尾声了,我个人的评价是相比去年,在进度管理和流程管理上做的更好了一些。对比今年一月份开会时开会的PPT,基本上当时的规划和目标基本上都达成了。这次聊聊整个研发过程中的一些感悟。 首先是对于整个团队的研发方向做了一个比较大的…

Socket网络编程

Socket编程 为了实现两台不同的机器能够进行通信,所有要使用到网络编程 IP地址与端口号IP地址:用于标识网络上主机的位置,每台网络上的设备都有唯一的ip地址。 端口号:用于标识主机上的哪个应用程序,一台主机上运行的很多应用程序,该数据是传送给哪个应用程序使用的通过端…

golang初学:交叉编译

go version go1.22.1 windows/amd64 Windows 11 + amd64 x86_64 x86_64 GNU/Linux ---序章 golang 支持 跨平台,支持的方式 是 在一个平台 编译其它平台的可执行程序。 本文 介绍 Windows 11(开发主机) 上 编译 Linux(目标主机) 上的可执行程序。#go build开发主机 和 目标…

DRF之三大认证

一、认证 1、自定义认证 在前面说的 APIView 中封装了三大认证,分别为认证、权限、频率。认证即登录认证,权限表示该用户是否有权限访问接口,频率表示用户指定时间内能访问接口的次数。整个请求最开始的也是认证。 (1)需求登陆认证 用户登陆成功--》签发token 以后需要登陆…

Vue入门到关门之Vue项目工程化

一、创建Vue项目 1、安装node环境官网下载,无脑下一步,注意别放c盘就行Node.js — Run JavaScript Everywhere (nodejs.org)需要两个命令npm---->pip node--->python装完检查一下,hello world检测,退出crtl+c2、搭建vue项目环境装cnpm 这个包,下载东西会快很多,装模…

第6讲需求分析--uml用例图

用例图 一.识别参与者 参与者在系统外所以画图时画在框外。系统用例图比业务用例图更完善所以经常让画的是系统用例图。在商品销售系统里:参与者是顾客。 在会计系统里:参与者是商品销售系统。 二.关系 1.参与者之间 泛化关系子指向父 2.参与者和用例之间 关联关系3.用例和用…