Bochs
调试工具跟踪Linux 0.11
的地址翻译(地址映射)过程,了解IA-32
(Intel Architecture 32-bit)的CPU
架构下的地址翻译和Linux 0.11
的内存管理机制。Ubuntu
上编写多进程的生产者-消费者程序,用共享内存做缓冲区(上一个实验是用文件做缓冲区)pc.c
程序上的应用)的基础上,为Linux 0.11
增加共享内存功能,并将生产者-消费者程序移植到Linux 0.11
。首先以汇编级调试的方式启动
Bochs
,引导Linux 0.11
,在0.11
下编译和运行程序test.c
,可以看到程序会一直空转,也就是陷入了死循环,永远不会主动退出程序,
#include
int i = 0x12345678;
int main(void)
{
printf("LQD The logical/virtual address of i is 0x%08x", &i);
fflush(stdout);
while (i)
;
return 0;
}
然后在调试器中通过查看程序(进程)的各项系数参数,从
GDT
表、逻辑地址、LDT
表、线性地址、页表、最后计算出变量i
的物理地址,最后通过直接修改物理内存的方式来让test.c
进程退出。
步骤:
1.编写test.c
程序(如上),并放置oslab/oslab
中
指令:
sudo ./mount-hdc //安装hdc目录里面的内容
cp test.c hdc/usr/root //将oslab中的test.c文件移动到hdc/usr/root下
2.启动汇编级调试方式(以前是./run
直接启动os
)
指令:
./dbg-asm
会显示如下所示的信息:
其中Next at = 0
表示下面的指令是Bochs
启动后要执行的第一条软件指令,
单步跟踪进去就能看到BIOS
的代码了。现在输入命令c
,即继续运行程序,Bochs
会和以前输入./run
一样启动os
。
3.在Linux 0.11
上编译test.c
得到可执行文件,然后运行test.c
:
指令:
gcc -o test test.c //编译test.c
./test //运行test程序
后面的0x00003004
就是这个程序中变量i
的逻辑地址,这个值在任何机器上都是一样的,在同一个机器上运行多次当然也是一样的。
可以看到程序一直在运行,没有退出,这也就是上面所说的陷入了死循环(正是因为进程不会退出,才可以在调试器中查看进程的各种资源: 逻辑地址、LDT表、GDT表和页表等信息)。
4.这个实验第一部分的目的就是一步步的找到这个变量i
的物理地址,然后将它改成0
,使得可以退出循环。
4.1 在命令行窗口输入ctrl+c
,Bochs
就会暂停运行,进入调试状态,命令行窗口会出现如下信息:
注:
1.如果000f
处位置显示的是0008
,表示按下Ctrl+c
键中断发生在内核中,
这时需要输入c
继续执行,然后在按下Ctrl+c
键,直到变成000f
为止。
2.如果cmp
处显示的不是这个,就用’n’命令单步运行几次,直到停在cmp
处,实际上就是要停留在test.c
的while(i)语句处,方便下面的继续调试。
4.2 使用反汇编指令u/7
,显示从当前位置(while(1)
)开始的7条指令的反汇编指令:
很容易分析出来变量i
存放的位置就是ds:0x3004
(就是上面输出的逻辑地址),
然后将变量i和0x00000000
位置的值相比较:
if 相等 (i==0)
就退出循环(jz ...)
else 不相等 (i!=0)
就继续往下执行(jmp ...),也就是继续执行循环体的内容
4.3 开始寻找和逻辑地址ds:0x3004
对应的物理地址,也就是去跟踪地址转换过程。
思路: 要从逻辑地址找到物理地址,首先要找到虚拟地址(因为这是段页式内存机制),所以需要找段表,段表其实就是进程的LDT表
,要找LDT表
,需要通过LDTR
寄存器,
LDT
在哪里呢?LDTR
寄存器是线索的起点,通过它可以在GDT
中找到LDT
的物理地址(实际上LDTR
不是直接指向对应的LDT
表的,而是存储GDT
表对于这个LDT
表的偏移)。
可以看到LDTR
寄存器的值是0x0068=0000000001101000(二进制)
,表示LDT
表存放在GDT
表的1101(二进制)
号位置,而且GDT
表的位置也已经由GDTR
寄存器给出,在物理地址0x00005cb8
下(不同的实验者可能会不一样,比如书上的就是0x00005cc8
,所以后面的数值也会有点不同,但是计算方法和思路是一样的)。
使用命令
xp /32w 0x00005cb8
查看GDT
表:
GDT
表中的每一项占64
位(8
个字节),所以要查找的项的地址是0x00005cb8 + 13*8
,使用命令:xp /2w 0x00005cb8 + 13*8
这里只要保证后面的两个数字和sreg
输出中,LDTR
后面的dl
和dh
值是不是一致。
0x52d00068 0x000082fd 将加粗的数字组合成:
=> 0x00fd52d0,这就是LDT
表的物理地址(GDT
表存放了所有进程的LDT
表)。
使用命令:xp /8w 0x00fd52d0
这就是LDT
表前四项的内容了,也就是已经找到了段表了。
先看看
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
为单位。
(上面这段话我其实看不太懂)
ds
段在线性地址空间的起始地址(也就是该进程对应的段在虚拟内存中的起始地址),用命令验证线性地址是不是对的:
calc ds:0x3004
,
发现是对的,当前已经找到了该进程在虚拟内存中的线性地址了,然后需要找物理地址
在IA-32(英特尔的32位CPU架构)下,页目录表的位置由
CR3
寄存器指引,用creg
命令可以看到:
说明页目录表的基址为0,看看其内容用命令:xp /68w 0
页目录表和页表中的内容很简单,是1024个32位数,这32位中前20位是物理页框号,后面是一些属性信息(最重要的是最后一位P),其中第65个页目录项就是要找的内容,
用命令查看:xp /w 0 + 64*4
其中的027是属性,显然P=1,这里还也可以分析出其它的属性。
页表所在的物理页框号为0x00fa6
,即页表在物理内存为0x00fa6000
处,从该位置开始查找3
号页表项(每个页表项4个字节),用命令:xp /w 0x00fa6000 + 3*4
其中的067是属性,显然P=1。
0x10003004
对应的物理页框号为0x00fa3
,和页内偏移0x004
接到一起,得到0x00fa3004
,这就是变量i的物理地址,可以通过两种方法验证:1.用命令:
page 0x10003004
,可以得到信息:“linear page 0x10003004 maps to physical page 0x00fa3004”
。
2.用命令:xp /w 0x00fa3004
:
然后已经找到了物理地址,但是还有最后一步: 直接修改物理地址的值,使得变量i为0
命令:setpmem 0x00fa3004 4 0
(表示从0x00fa3004地址开始设置4个字节都为0)
然后输入命令c
(如上),继续从调试状态的程序运行下去,就会看到在Bochs中程序顺利退出:
到这里第一个部分也正式完成了。
说明: 第二个部分的实验博主并没有自己运行出结果,只是在最后标注的两份实验报告的基础上进行理解,而且下面的代码都是基于真实的Ubuntu环境下实现的。
本实验是在Ubuntu
下完成的,与上一个信号量实验中的pc.c
的功能要求基本一致,
仅有两点不一样:
pc.c
,而是生产者是producer.c
,消费者是consumer.c
,两个程序都是单进程的,通过信号量和共享缓冲区进行进程间的通信。Linux
下,可以通过shmget()
和shmat()
两个系统调用使用共享内存,当然Linux 0.11
本身是没有这两个系统调用的,要自己来实现。
Linux
支持两种方式的共享内存。一种方式是shm_open()
、mmap()
和shm_unlink()
的组合;另一种方式是shmget()
、shmat()
和shmdt()
的组合。本实验建议使用后一种方式。
shmget()
系统调用的函数原型:
int shmget(key_t key, size_t size, int shmflg);
该系统调用会新建或者打开一页物理内存作为共享内存,返回该共享内存的shmid
,即该页共享内存在操作系统中的标识,如果多个进程使用相同的key
调用shmget()
,则这些进程就会获得相同的shmid
,即得到了同一块共享内存的标识。
在shmget()
实现时,
如果key
所对应的内存已经建立,直接返回shmid
,否则新建然后再返回。
如果size
超过一页内存大小,返回-1
,并置errno
为EINVAL
。
如果系统没有空闲内存了,返回-1
,并置errno
为ENOMEM
。
参数shmflg
在本次实验中可以直接忽略。
shmat()
系统调用的函数原型为:
void * shmat(int shmid, const void * shmaddr, int shmflg);
该系统调用会将shmid
对应的共享内存页面映射到当前进程的虚拟地址空间中,并返回一个逻辑地址p,调用进程可以读写逻辑地址p来读写这一页共享内存。两个进程都调用shmat
可以关联到同一页物理内存上,此时两个进程读写p指针就是在读写同一页内存,从而实现了基于共享内存的进程间通信。
如果shmid
不正确,返回-1
,并置errno
为EINVAL
。
参数shmaddr
和shmflg
在本次实验中可以直接忽略。
本实验的这个部分就是要实现如下所示的图,也就是不同的进程要共享同一段内存空间,实现基于共享内存的进程间通信。
观察一下上面的图,不难发现要从后往前来一步步建立:
要获得一个空闲的物理内存空间,即调用函数get_free_page()
(这是shmget()
系统调用要完成的主要工作)。
将shmget()
创建好的物理共享内存页面 和进程对应的虚拟内存 以及逻辑地址关联起来,让进程对某个逻辑地址的读写就是对这个共享物理地址进行读写。
补一个知识点:
父子进程共享同一块物理页框,这个页框需要设置成只读,因为如果父子进程都可以写,那么两个进程在并发执行时就会相互影响使得内存空间的信息出现错误。
但是进程的读写是最基本的操作,那当要写内存的时候该怎么做呢?
一旦写内存的时候就会出现读写异常中断(有写好的中断处理函数),中断处理的结果就是给要写的进程新分配一个内存空间,这个进程对应的页表也要改值指向新的物理内存空间。
这就是著名的写时复制机制。
LInux0.11
中提供了这样一个函数:put_page(tmp,address);//tmp为虚拟地址,address为物理地址
address
就是第一步用shmget()
创建的共享内存对应的地址,所以这里建立起映射关系的核心操作就是得到虚拟地址tmp
,即是要从进程虚拟内存空间划分出一段空间。要在进程的虚拟内存区域划分出一段空间, 首先需要了解进程虚拟空间的分布,阅读源码(主要是exec.c
)可以发现每个进程在虚拟内存空间会分配64MB
的大小,分别有代码段、数据段、堆段(用来存放程序中未初始化的全局变量的一个段)、栈段,从图(待制作)看出,堆段到栈段的虚拟子内存空间是没有被使用的,我们可以在这里分割出一个页表来建立起到物理内存的映射关系。 问题: 虚拟内存不是实际存在的,为什么在这里可以建立一个页表
这些段的信息都是存储在进程PCB
中的,所以可以用current->brk
找到从进程开始处到brk
位置的长度,当然这还不是brk
所在的虚拟地址,只是离分配给该进程的64MB
虚拟内存开始处的偏移地址,current->brk
再加上该进程开始处的虚拟地址才是我们想要的虚拟地址,当前进程开始处的虚拟地址存放在current->ldt[1]
中,可以用get_base(current->ldt[1])
获取,所以经过:
tmp = get_base(current->ldt[1]) + current->brk;//当前进程的虚拟地址 + brk在虚拟内存中的偏移 = brk的虚拟地址
计算出虚拟地址tmp
以后,就可以用put_page(tmp,address)
函数来创建页表了,也就可以建立起虚拟地址到物理地址的映射关系了。
PCB
中存放的IDT表
,现在需要做的就是根据段表将虚拟地址反推得到逻辑地址。因为,逻辑地址 + 段基址 = 虚拟地址,所以,逻辑地址 = 虚拟地址 - 段基址,也就是:
logicalAddress = tmp - get_base(current->ldt[1])
现在算出了逻辑地址,只需要通过shmat()
返回给用户程序,用户程序操作这个逻辑地址实际上就是在操作那一页用shmget()
建立起来的共享物理内存区域了。
代码实现:
shm.c
#include
#include
#include
#include
#include
shm_t shms[SHM_SIZE] = {{0, 0, 0}};
/**
* 该文件用于实现对共享物理内存空间的操作
* 1.创建
* 2.返回逻辑地址,以便于生产者-消费者读写
*/
/**
* 该系统调用会新建或者打开一页物理内存作为共享内存,返回该共享内存的shmid,
* 即该页共享内存在操作系统中的标识,如果多个进程使用相同的key调用shmget(),
* 则这些进程就会获得相同的shmid,即得到了同一块共享内存的标识。
* 在shmget()实现时,
* 如果key所对应的内存已经建立,直接返回shmid,否则新建然后再返回。
* 如果size超过一页内存大小,返回-1,并置errno为EINVAL。
* 如果系统没有空闲内存了,返回-1,并置errno为ENOMEM。
* 参数shmflg在本次实验中可以直接忽略。
*/
int shmget(key_t key, size_t size/*, int shmflg*/)
{
int i;
//得到原来已经创建好的共享内存(在生产者已经创建好了的共享内存,消费者拿着相同的key对应的是同一块内存)
for(i = 0; i < SHMS_SIZE; i++)
{
if(shms[i].key == key)
{
return i;
}
}
//创建新的共享内存
for(i = 0; i < SHMS_SIZE; i++);
if(i == SHMS_SIZE)
{
printk("no shm place");
errno = ENOMEM;
return -1;
}
if(size > PAGE_SIZE)
{
errno = EINVAL;
return -1;
}
shms[i].size = size;
shms[i].key = key;
if(!(shms[i].addr = get_free_page()))
{
return ENOMEM;
}
return i;
}
/**
* 该系统调用会将shmid对应的共享内存页面映射到当前进程的虚拟地址空间中,并返回一个逻辑地址p,
* 调用进程可以读写逻辑地址p来读写这一页共享内存。
* 两个进程都调用shmat可以关联到同一页物理内存上,此时两个进程读写p指针就是在读写同一页内存,
* 从而实现了基于共享内存的进程间通信。
* 如果shmid不正确,返回-1,并置errno为EINVAL。
* 参数shmaddr和shmflg在本次实验中可以直接忽略。
*/
void * shmat(int shmid/*, const void * shmaddr, int shmflg*/);
{
unsigned long logical_addr;
if(shmid < 0 || shmid >= SHMS_SIZE)
{
//shmid对应的物理内存不存在
errno = EINVAL;
return -1;
}
logival_addr = current->brk;
current->brk += PAGE_SIZE;
if(!put_page(shms[shmid].addr,current->start_code+logival_addr))
{
//分页分不出来
errno = EINVAL;
return -1;
}
//返回共享物理空间对应的逻辑地址
return (void)*logival_addr;
}
上述的过程只是1.建立了一个共享内存空间 2.得到了这个共享内存空间的逻辑地址,得到了地址然后要做什么呢?
当然就是要操作这个内存空间,所以就引出了用生产者—消费者进程来应用这个创建好的共享物理内存作为共享缓冲区,上个实验可以看到是用文件作为共享缓冲区,所以这是和上个实验(信号量的实现与应用)第一个不同点,
第二个不同点这次是把生产者和消费者都作为不同的程序(produce.c
consumer.c
)。
producer.c
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 10
#define COUNT 500
#define KEY 183
#define SHM_SIZE (BUF_SIZE+1)*sizeof(short)
int main(int argc,char ** argv)
{
int pid;//该进程的id
unsigned short count = 0;//生产资源的个数
int shm_id;//共享物理内存空间的id
short *shmp;//操作共享内存的逻辑地址
sem_t *empty;//三个实现进程间的同步
sem_t *full;
sem_t *mutex;
//关闭原来的信号量
sem_unlink("empty");
sum_unlink("full");
sum_unlink("mutex");
//新打开三个信号量
empty = sem_open("empty",0_CREAT|O_EXCL,0666,10);
full = sem_open("full",0_CREAT|O_EXCL,0666,0);
mutex = sem_open("mutex",0_CREAT|O_EXCL,0666,1);
if(empty == SEM_FAILED || full == SEM_FAILED || mutex == SEM_FAILED)
{
//申请信号量失败
printf("sem_open error!\n");
return -1;
}
//使用KEY值申请一块共享物理内存
shm_id = shmget(KEY,SHM_SIZE,IPC_CREAT|0666);
if(shm_id == -1)
{
//申请共享内存失败
printf("shmget error!\n");
return -1;
}
shmp = (short*)shmat(shm_id,NULL,0);//返回共享物理内存的逻辑地址
pid = syscall(SYS_getpid);//得到进程的pid
//生产者生产出资源
while(count <= COUNT)
{
sem_wait(empty);//P(empty)
sem_wait(mutex);//P(mutex)
printf("Producer 1 process %d : %d\n",pid,count);
fflush(stdout);
*(shmp++) = count++;
if(!(count % BUF_SIZE))
{
shmp -= 10;
}
sem_post(mutex);//V(mutex)
sem_post(full);//V(full)
}
return 0;
}
consumer.c
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 10
#define KEY 183
int main()
{
int pid;
int shm_id;
short *shmp;
short *index;
sem_t *empty;
sem_t *full;
sem_t *mutex;
shm_id = shmget(KEY,0,0);//使用和生产者同一个KEY值,会返回同一个shm_id(指向同一个内存空间)
if(shm_id == -1)
{
//申请共享内存失败
printf("shmget error!\n");
return -1;
}
shmp = (short*)shmat(shm_id,NULL,0);//返回共享物理内存的逻辑地址
index = shmp + BUF_SIZE;
*index = 0;
//打开生产者那里创建的三个信号量
empty = sem_open("empty",0);
full = sem_open("full",0);
mutex = sem_open("mutex",0);
if(empty == SEM_FAILED || full == SEM_FAILED || mutex == SEM_FAILED)
{
//申请信号量失败
printf("sem_open error!\n");
return -1;
}
if(!sysvall(SYS_fork))
{
pid = syscall(SYS_getpid);//得到进程的pid
//消费者1开始消费资源
while(1)
{
sem_wait(full);//P(full)
sem_wait(mutex);//P(mutex)
printf("Consumer 1 process %d : %d\n",pid,shem[*index]);
fflush(stdout);
if(*index == 9)
{
*index = 0;
}
else
{
(*index)++;
}
sem_post(mutex);//V(mutex)
sem_post(empty);//V(empry)
}
return 0;
}
if(!sysvall(SYS_fork))
{
pid = syscall(SYS_getpid);//得到进程的pid
//消费者2开始消费资源
while(1)
{
sem_wait(full);
sem_wait(mutex);
printf("Consumer 2 process %d : %d\n",pid,shem[*index]);
fflush(stdout);
if(*index == 9)
{
*index = 0;
}
else
{
(*index)++;
}
sem_post(mutex);
sem_post(empty);
}
return 0;
}
if(!sysvall(SYS_fork))
{
pid = syscall(SYS_getpid);//得到进程的pid
//消费者3开始消费资源
while(1)
{
sem_wait(full);
sem_wait(mutex);
printf("Consumer 3 process %d : %d\n",pid,shem[*index]);
fflush(stdout);
if(*index == 9)
{
*index = 0;
}
else
{
(*index)++;
}
sem_post(mutex);
sem_post(empty);
}
return 0;
}
return 0;
}
由于Linux 0.11
只有一个终端,而现在需要在同一个终端下同时运行两个程序(上述的生产者和消费者程序)。
Linux
的shell
有后台运行程序的功能,只要在命令的末尾输入一个&
,命令就会自动进入到后台执行,前台会马上回到提示符状态,然后运行下一个命令,例如:
# ./producer &
# ./consumer
生产者进程会在后台运行,产生资源到共享内存空间,然后消费者会在前台运行,消费共享内存空间的资源,从而可以通过这个共享内存来实现进程间的相互通信了
1. 对于地址映射实验部分(第一部分),列出你认为最重要的几步(不超过四步),并给出你获得的实际数据。
u/7
反汇编,查看变量i
对应的逻辑地址IDT
表,然后IDT
表要根据LDTR
寄存器和GDT
表,对应的命令就是sreg
ds
(代码段)寄存器查找IDT
表,得到基址,然后通过基址 + 逻辑地址 = 虚拟地址setpmem (物理地址) 4 0
来修改变量i
的值,然后命令c
继续运行就可以退出程序了。2.
test.c
退出后,如果马上再运行一次程序,并再进行地址跟踪,你会发现哪些异同?为什么?
i
重新被赋非0值,所以仍然还是会死循环。HIT-OS-LAB参考资料:
1.《操作系统原理、实现与实践》-李治军、刘宏伟 编著
2.《Linux内核完全注释》
3.两个哈工大同学的实验源码
4.Linux-0.11源代码
(上述资料,如果有需要的话,请主动联系我))
该实验的参考资料
网课
官方文档
参考实验报告_1
参考实验报告_2
返回顶部