跟踪地址翻译过程
1.准备
./dbg-asm c
在Bochs中编译运行 test.c
只要test.c不变,0x00003004这个值在任何人的机器上都是一样的。即使在同一个机器上多次运行test.c,也是一样的。
test.c是一个死循环,只会不停占用CPU,不会退出。
2.暂停
在命令行窗口按 Ctrl+c,Bochs 会暂停运行,进入调试状态
其中的 000f 如果是 0008,则说明中断在了内核里。那么就要 c,然后再 ctrl+c,直到变为 000f 为止。
如果显示的下一条指令不是 cmp ...(这里指语句以 cmp 开头),就用 n 命令单步运行几步,直到停在 cmp ...。
使用命令 u /8,显示从当前位置开始 8 条指令的反汇编代码,结构如下:
这就是 test.c 中从 while 开始一直到 return 的汇编代码。变量 i 保存在 ds:0x3004 这个地址,并不停地和 0 进行比较,直到它为 0,才会跳出循环。
现在,开始寻找 ds:0x3004 对应的物理地址。
3.段表
ds:0x3004 是虚拟地址,ds 表明这个地址属于 ds 段。首先要找到段表,然后通过 ds 的值在段表中找到 ds 段的具体信息,才能继续进行地址翻译。
每个在 IA-32 上运行的应用程序都有一个段表,叫 LDT,段的信息叫段描述符。
LDT 在哪里呢?ldtr 寄存器是线索的起点,通过它可以在 GDT(全局描述符表)中找到 LDT 的物理地址。
用 sreg 命令(是在调试窗口输入):
可以看到 ldtr 的值是 0x0068=0000000001101000(二进制),表示 LDT 表存放在 GDT 表的 1101(二进制)=13(十进制)号位置(每位数据的意义参考后文叙述的段选择子)。
而 GDT 的位置已经由 gdtr 明确给出,在物理地址的 0x00005cb8。
用 xp /32w 0x00005cb8 查看从该地址开始,32 个字的内容,及 GDT 表的前 16 项,如下:
GDT 表中的每一项占 64 位(8 个字节),所以我们要查找的项的地址是 0x00005cb8+13*8。
输入 xp /2w 0x00005cb8+13*8,得到:
“0x52d00068 0x000082fd” 将其中的加粗数字组合为“0x00faa2d0”,这就是 LDT 表的物理地址(为什么这么组合,参考后文介绍的段描述符)。
xp /8w 0x00fd52d0,得到:
4.段描述符
在保护模式下,段寄存器有另一个名字,叫段选择子,因为它保存的信息主要是该段在段表里索引值,用这个索引值可以从段表中“选择”出相应的段描述符。
先看看 ds 选择子的内容,还是用 sreg 命令:
可以看到,ds 的值是 0x0017。段选择子是一个 16 位寄存器,它各位的含义如下图:
其中 RPL 是请求特权级,当访问一个段时,处理器要检查 RPL 和 CPL(放在 cs 的位 0 和位 1 中,用来表示当前代码的特权级),即使程序有足够的特权级(CPL)来访问一个段,但如果 RPL(如放在 ds 中,表示请求数据段)的特权级不足,则仍然不能访问,即如果 RPL 的数值大于 CPL(数值越大,权限越小),则用 RPL 的值覆盖 CPL 的值。
而段选择子中的 TI 是表指示标记,如果 TI=0,则表示段描述符(段的详细信息)在 GDT(全局描述符表)中,即去 GDT 中去查;而 TI=1,则去 LDT(局部描述符表)中去查。
看看上面的 ds,0x0017=0000000000010111(二进制),所以 RPL=11,可见是在最低的特权级(因为在应用程序中执行),TI=1,表示查找 LDT 表,索引值为 10(二进制)= 2(十进制),表示找 LDT 表中的第 3 个段描述符(从 0 开始编号)。
LDT 和 GDT 的结构一样,每项占 8 个字节。所以第 3 项 0x00003fff 0x10c0f300(上一步骤的最后一个输出结果中) 就是搜寻好久的 ds 的段描述符了。
用 sreg 输出中 ds 所在行的 dl 和 dh 值可以验证找到的描述符是否正确。
接下来看看段描述符里面放置的是什么内容:
可以看到,段描述符是一个 64 位二进制的数,存放了段基址和段限长等重要的数据。其中位 P(Present)是段是否存在的标记;位 S 用来表示是系统段描述符(S=0)还是代码或数据段描述符(S=1);四位 TYPE 用来表示段的类型,如数据段、代码段、可读、可写等;DPL 是段的权限,和 CPL、RPL 对应使用;位 G 是粒度,G=0 表示段限长以位为单位,G=1 表示段限长以 4KB 为单位;其他内容就不详细解释了。
5.段基址和线性地址
组合规则见段描述符结构:其实就是将基地址进行组合
费了很大的劲,实际上我们需要的只有段基址一项数据,即段描述符 “0x00003fff 0x10c0f300” 中加粗部分组合成的 “0x10000000”。这就是 ds 段在线性地址空间中的起始地址。用同样的方法也可以算算其它段的基址,都是这个数。
段基址+段内偏移,就是线性地址了。所以 ds:0x3004 的线性地址就是:
0x10000000 + 0x3004 = 0x10003004
用 calc ds:0x3004 命令可以验证这个结果。
6.页表
从线性地址计算物理地址,需要查找页表。线性地址变成物理地址的过程如下:
首先需要算出线性地址中的页目录号、页表号和页内偏移,它们分别对应了 32 位线性地址的 10 位 + 10 位 + 12 位,所以 0x10003004 的页目录号是 64,页号 3,页内偏移是 4。
IA-32 下,页目录表的位置由 CR3 寄存器指引。“creg”命令可以看到:
说明页目录表的基址为 0。看看其内容,“xp /68w 0”:
页目录表和页表中的内容很简单,是 1024 个 32 位(正好是 4K)数。这 32 位中前 20 位是物理页框号,后面是一些属性信息(其中最重要的是最后一位 P)。其中第 65 个页目录项就是我们要找的内容,用“xp /w 0+64*4”查看:
这就是我们要找的页目录表
里面的内容是 0x00fa6027
具体分析一下里面的结构
页表所在的物理页框号为0x00fa6,即页表在物理内存为0x00fa6000处,从该位置开始查找3号页表项(每个页表项4个字节),用命令:xp /w 0x00fa6000 + 3*4
7.物理地址
页表里所显示的即线性地址 0x10003004 对应的物理页框号为 0x00fa5,和页内偏移 0x004 连接在一起,得到 0x00fa5004,这就是变量 i 的物理地址。
可以用两种方法验证:
第一种方法是用命令 page 0x10003004
第二种方法是用命令 xp /w 0x00fa5004
现在,通过直接修改内存来改变 i 的值为 0,命令是: setpmem 0x00fa5004 4 0,表示从 0x00fa5004 地址开始的 4 个字节都设为 0。然后再用“c”命令继续 Bochs 的运行,可以看到 test 退出了,说明 i 的修改成功了,此项实验结束。
添加系统调用号
目录 oslab/linux-0.11/include
在 unistd.h 中添加下面代码
然后增加两个系统调用号
添加系统调用的定义
文件位置oslab/linux0.11/include/linux/sys.h
增加函数声明
改写系统调用数
文件位置:oslab/linux0.11/kernel/system_call.s
编写 shm.c
文件位置:oslab/linux0.11/kernel/shm.s
#define __LIBRARY__ #include <unistd.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/mm.h> #include <errno.h> static shm_ds shm_list[SHM_SIZE] = {{0, 0, 0}}; int sys_shmget(unsigned int key, size_t size) { int i; unsigned long page; /* If the size exceeds the size of one page of memory */ if (size > PAGE_SIZE) { printk("shmget: size %u cannot be greater than the page size %ud. \n", size, PAGE_SIZE); return -ENOMEM; } if (key == 0) { printk("shmget: key cannot be 0.\n"); return -EINVAL; } for (i = 0; i < SHM_SIZE; i++) { if (shm_list[i].key == key) { return i; } } page = get_free_page(); if (!page) { return -ENOMEM; } printk("shmget get memory's address is 0x%08x\n", page); for (i = 0; i < SHM_SIZE; i++) { if (shm_list[i].key == 0) { shm_list[i].key = key; shm_list[i].size = size; shm_list[i].page = page; return i; } } return -1; } void *sys_shmat(int shmid) { unsigned long data_base, brk; if (shmid < 0 || SHM_SIZE <= shmid || shm_list[shmid].page == 0 || shm_list[shmid].key <= 0) { return (void *)-EINVAL; } data_base = get_base(current->ldt[2]); printk("current's data_base = 0x%08x,new page = 0x%08x\n", data_base, shm_list[shmid].page); brk = current->brk + data_base; current->brk += PAGE_SIZE; if (put_page(shm_list[shmid].page, brk) == 0) { return (void *)-ENOMEM; } return (void *)(current->brk - PAGE_SIZE); }
brk指针在哪里?可以参考下图。我们可以发现 brk 指针正好指向堆区顶部,我们可以利用这个指针帮我们定位需要申请的空间首地址。
在进程PCB当中记录了brk指针的 逻辑地址,然后加上进程开始处的虚拟地址就可以得到brk指针的虚拟地址或者叫线性地址(虚拟地址=段基址+逻辑地址)