本次实验内容:
- 用Bochs调试工具跟踪Linux 0.11的地址翻译(地址映射)过程,了解IA-32和Linux 0.11的内存管理机制;
- 在Ubuntu上编写多进程的生产者—消费者程序,用共享内存做缓冲区;
(一)地址映射这个过程不好理解
在复制实验指导书之前我想说下 linux虚拟内存机制;
- linux采用段页结合的方式.
段为虚拟地址,页找到物理地址 - 程序的地址分代码段,数据段这样一段段保存,用段基址+偏移量来标记
- 每个段在LDT这个段表记录着 每一个段的逻辑基地址;
- LDT的物理地址可以在GDT(全局描述符表)找到.
找到LDT 再根据段号 去找到段基地址,加上偏移量就等于线性虚拟地址. - 找到虚拟地址 找物理地址就是找页目录,页表,页偏移量的问题了
- 所有这些地址注意转化成二进制,然后懂得每一位的意义;
*段选择符是一个64位的数,所以占8个字节,分高4字节跟低4字节
前低后高 31-0这样排列
实验指导书关于这个过程很详细,我就复制粘贴了:
编译好Linux 0.11后,首先通过运行./dbg-asm启动调试器,此时Bochs的窗口处于黑屏状态,而命令行窗口显示:
“Next at t=0”表示下面的指令是Bochs启动后要执行的第一条软件指令。单步跟踪进去就能看到bios的代码。不过这不是本实验需要的。直接输入命令“c”,continue程序的运行,Bochs一如既往地启动了Linux 0.11。
在Linux 0.11下输入(或拷入)test.c,编译为test,运行之,打印如下信息:
The logical/virtual address of i is 0x00003004
只要test不变,0x00003004这个值在任何人的机器上都是一样的。即使在同一个机器上多次运行test,也是一样的。
test是一个死循环,只会不停占用CPU,不会退出。
暂停
当test运行的时候,在命令行窗口按“ctrl+c”,Bochs会暂停运行,进入调试状态。绝大多数情况下都会停在test内,显示类似如下信息:
(0) [0x00fc8031] 000f:00000031 (unk. ctxt): cmp dword ptr ds:0x3004, 0x00000000 ; 833d0430000000
其中加粗的“000f”如果是“0008”,则说明中断在了内核里。那么就要c,然后再ctrl+c,直到变为“000f”为止。如果显示的下一条指令不是“cmp ...”,就用“n”命令单步运行几步,直到停在“cmp ...”。
使用命令“u /7”,显示从当前位置开始7条指令的反汇编代码,如下:
10000031: ( ): cmp dword ptr ds:0x3004, 0x00000000 ; 833d043000000010000038: ( ): jz .+0x00000002 ; 74021000003a: ( ): jmp .+0xfffffff5 ; ebf51000003c: ( ): xor eax, eax ; 31c01000003e: ( ): jmp .+0x00000000 ; eb0010000040: ( ): leave ; c910000041: ( ): ret ; c3
这就是test.c中从while开始一直到return的汇编代码。变量i保存在ds:0x3004这个地址,并不停地和0进行比较,直到它为0,才会跳出循环。
现在,开始寻找ds:0x3004对应的物理地址。
段表
ds:0x3004是虚拟地址,ds表明这个地址属于ds段。首先要找到段表,然后通过ds的值在段表中找到ds段的具体信息,才能继续进行地址翻译。每个在IA-32上运行的应用程序都有一个段表,叫LDT,段的信息叫段描述符。
LDT在哪里呢?ldtr寄存器是线索的起点,通过它可以在GDT(全局描述符表)中找到LDT的物理地址。
用“sreg”命令:
cs:s=0x000f, dl=0x00000002, dh=0x10c0fa00, valid=1ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3ss:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1es:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1fs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1gs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1ldtr:s=0x0068, dl=0xc2d00068, dh=0x000082f9, valid=1tr:s=0x0060, dl=0x52e80068, dh=0x00008bfd, valid=1gdtr:base=0x00005cc8, limit=0x7ffidtr:base=0x000054c8, limit=0x7ff
可以看到ldtr的值是0x0068=0000000001101000(二进制),表示LDT表存放在GDT表的1101(二进制)=13(十进制)号位置(每位数据的意义参考后文叙述的段选择子)。而GDT的位置已经由gdtr明确给出,在物理地址的0x00005cc8。用“xp /32w 0x00005cc8”查看从该地址开始,32个字的内容,即GDT表的前16项,如下:
0x00005cc8 : 0x00000000 0x00000000 0x00000fff 0x00c09a000x00005cd8 : 0x00000fff 0x00c09300 0x00000000 0x000000000x00005ce8 : 0xa4280068 0x00008901 0xa4100068 0x000082010x00005cf8 : 0xf2e80068 0x000089ff 0xf2d00068 0x000082ff0x00005d08 : 0xd2e80068 0x000089ff 0xd2d00068 0x000082ff0x00005d18 : 0x12e80068 0x000089fc 0x12d00068 0x000082fc0x00005d28 : 0xc2e80068 0x00008bf9 0xc2d00068 0x000082f90x00005d38 : 0x00000000 0x00000000 0x00000000 0x00000000
GDT表中的每一项占64位(8个字节),所以我们要查找的项的地址是“0x00005cc8 + 13 * 8”。“xp /2w 0x00005cc8 + 13 * 8”,得到:
0x00005d30 : 0xc2d00068 0x000082f9
上两步看到的数值可能和这里给出的示例不一致,这是很正常的。如果想确认是否准确,就看sreg输出中,ldtr所在行里,dl和dh的值,它们是Bochs的调试器自动计算出的,你寻找到的必须和它们一致。
“0xc2d00068 0x000082f9”将其中的加粗数字组合为“0x00f9c2d0”,这就是LDT表的物理地址(为什么这么组合,参考后文介绍的段描述符)。“xp /8w 0x00f9c2d0”,得到:
0x00f9c2d0 : 0x00000000 0x00000000 0x00000002 0x10c0fa000x00f9c2e0 : 0x00003fff 0x10c0f300 0x00000000 0x00f9d000
这就是LDT表的前4项内容了。
段描述符
在保护模式下,段寄存器有另一个名字,叫段选择子,因为它保存的信息主要是该段在段表里索引值,用这个索引值可以从段表中“选择”出相应的段描述符。
先看看ds选择子的内容,还是用“sreg”命令:
cs:s=0x000f, dl=0x00000002, dh=0x10c0fa00, valid=1ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3ss:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1es:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1fs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1gs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1ldtr:s=0x0068, dl=0x52d00068, dh=0x000082fd, valid=1tr:s=0x0060, dl=0x52e80068, dh=0x00008bfd, valid=1gdtr:base=0x00005cc8, limit=0x7ffidtr:base=0x000054c8, limit=0x7ff
可以看到,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为单位;其他内容就不详细解释了。
段基址和线性地址
费了很大的劲,实际上我们需要的只有段基址一项数据,即段描述符“0x00003fff 0x10c0f300”中加粗部分组合成的“0x10000000”。这就是ds段在线性地址空间中的起始地址(ds段的段基址)。用同样的方法也可以算算其它段的基址,都是这个数。
段基址+段内偏移,就是线性地址了。所以ds:0x3004的线性地址就是:
0x10000000 + 0x3004 = 0x10003004
用“calc ds:0x3004”命令可以验证这个结果。
页表
从线性地址计算物理地址,需要查找页表。线性地址变成物理地址的过程如下:
首先需要算出线性地址中的页目录号、页表号和页内偏移,它们分别对应了32位线性地址的10位+10位+12位,所以0x10003004的页目录号是64,页号3,页内偏移是4。
IA-32下,页目录表的位置由CR3寄存器指引。“creg”命令可以看到:
CR0=0x8000001b: PG cd nw ac wp ne ET TS em MP PECR2=page fault laddr=0x10002f68CR3=0x00000000PCD=page-level cache disable=0PWT=page-level writes transparent=0CR4=0x00000000: osxmmexcpt osfxsr pce pge mce pae pse de tsd pvi vme
说明页目录表的基址为0。看看其内容,“xp /68w 0”:
0x00000000 : 0x00001027 0x00002007 0x00003007 0x000040270x00000010 : 0x00000000 0x00024764 0x00000000 0x000000000x00000020 : 0x00000000 0x00000000 0x00000000 0x000000000x00000030 : 0x00000000 0x00000000 0x00000000 0x000000000x00000040 : 0x00ffe027 0x00000000 0x00000000 0x000000000x00000050 : 0x00000000 0x00000000 0x00000000 0x000000000x00000060 : 0x00000000 0x00000000 0x00000000 0x000000000x00000070 : 0x00000000 0x00000000 0x00000000 0x000000000x00000080 : 0x00ff3027 0x00000000 0x00000000 0x000000000x00000090 : 0x00000000 0x00000000 0x00000000 0x000000000x000000a0 : 0x00000000 0x00000000 0x00000000 0x000000000x000000b0 : 0x00000000 0x00000000 0x00000000 0x00ffb0270x000000c0 : 0x00ff6027 0x00000000 0x00000000 0x000000000x000000d0 : 0x00000000 0x00000000 0x00000000 0x000000000x000000e0 : 0x00000000 0x00000000 0x00000000 0x000000000x000000f0 : 0x00000000 0x00000000 0x00000000 0x00ffa0270x00000100 : 0x00faa027 0x00000000 0x00000000 0x00000000
页目录表和页表中的内容很简单,是1024个32位(正好是4K)数。这32位中前20位是物理页框号,后面是一些属性信息(其中最重要的是最后一位P)。其中第65个页目录项就是我们要找的内容,用“xp /w 0+64 4”查看:
0x00000100 : 0x00faa027
其中的027是属性,显然P=1,其他属性实验者自己分析吧。页表所在物理页框号为0x00faa,即页表在物理内存的0x00faa000位置。从该位置开始查找3号页表项,得到(xp /w 0x00faa000+34):
0x00faa00c : 0x00fa7067
其中067是属性,显然P=1,应该是这样。
物理地址
最终结果马上就要出现了!
线性地址0x10003004对应的物理页框号为0x00fa7,和页内偏移0x004接到一起,得到0x00fa7004,这就是变量i的物理地址。可以通过两种方法验证。
第一种方法是用命令“page 0x10003004”,可以得到信息:“linear page 0x10003000 maps to physical page 0x00fa7000”。
第二种方法是用命令“xp /w 0x00fa7004”,可以看到:
0x00fa7004 : 0x12345678
这个数值确实是test.c中i的初值。
现在,通过直接修改内存来改变i的值为0,命令是: setpmem 0x00fa7004 4 0,表示从0x00fa7004地址开始的4个字节都设为0。然后再用“c”命令继续Bochs的运行,可以看到test退出了,说明i的修改成功了,此项实验结束。
总结一下需要找的过程:
- 找段号跟段偏移量
- 找LDT跟GDT
- 根据GDT的段描述符找到LDT的段描述符拼凑段基址
- 根据段基址+偏移量算出 线性虚拟地址
- 找页目录表
- 根据页目录找到页表 进行地址翻译
(二)共享内存
共享内存用函数:
int shmget(key_t key, size_t size, int shmflg);
第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
第二个参数,size以字节为单位指定需要共享的内存容量
第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间
void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一个参数,shm_id是由shmget函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
product.c文件:
#define __LIBRARY__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define Total 500
#define BUFFERSIZE 10
int main()
{
int shmid;
int put_pos = 0, i;
int *ShareAddress;
sem_t *empty, *full, *mutex;
empty = (sem_t *)sem_open("empty", O_CREAT, 0777, 10);
full = (sem_t *)sem_open("full", O_CREAT, 0777, 0);
mutex = (sem_t *)sem_open("mutex",O_CREAT, 0777, 1);
shmid = shmget( 555204, BUFFERSIZE*sizeof(int), IPC_CREAT|0666);
/*
if(!shmid)
{
printf("shmget failed!");
exit(0);
}
*/
ShareAddress = (int*)shmat(shmid, NULL, 0);
/*
if(!ShareAddress)
{
printf("shmat failed!");
exit(0);
}
*/
for( i = 0 ; i < Total; i++)
{
sem_wait(empty);
sem_wait(mutex);
ShareAddress[put_pos] = i;
put_pos = ( put_pos + 1 ) % BUFFERSIZE;
sem_post(mutex);
sem_post(full);
}
while(1);
return 0;
}
consumer.c文件:
#define __LIBRARY__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define Total 500
#define BUFFERSIZE 10
int main()
{
int shmid;
int get_pos = 0, i;
int *ShareAddress;
sem_t *empty, *full, *mutex;
empty = (sem_t *)sem_open("empty", O_CREAT, 0777, 10);
full = (sem_t *)sem_open("full", O_CREAT, 0777, 0);
mutex = (sem_t *)sem_open("mutex",O_CREAT, 0777, 1);
shmid = shmget( 555204, BUFFERSIZE*sizeof(int), IPC_CREAT|0666 ); /*返回共享内存的标识符*/
/* if(!shmid)
{
printf("shmget failed!");
fflush(stdout);
exit(0);
}
*/
ShareAddress = (int*)shmat(shmid, NULL, 0);
/*
if(!ShareAddress)
{
printf("shmat failed!");
fflush(stdout);
exit(0);
}
*/
for(i = 0; i < Total; i++)
{
sem_wait(full);
sem_wait(mutex);
printf("%d\n", ShareAddress[get_pos]);
fflush(stdout);
get_pos = ( get_pos + 1 ) % BUFFERSIZE;
sem_post(mutex);
sem_post(empty);
}
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
return 0;
}