Skip to content

Latest commit

 

History

History
495 lines (371 loc) · 17.8 KB

lab_7.md

File metadata and controls

495 lines (371 loc) · 17.8 KB

跟踪Linux 0.11地址翻译过程

用Bochs调试工具跟踪Linux 0.11的地址翻译(地址映射)过程,了解IA-32和Linux 0.11的内存管理机制。

以汇编级调试的方式启动bochs,引导Linux 0.11,在0.11下编译和运行test.c。它是一个无限循环的程序,永远不会主动退出。然后在调试器中通过查看各项系统参数,从逻辑地址、LDT表、GDT表、线性地址到页表,计算出变量i的物理地址。最后通过直接修改物理内存的方式让test.c退出运行。

首先看看test.c的代码:

#include <stdio.h>
int i = 0x12345678;
int main(void)
{
    printf("The logical/virtual address of i is 0x%08x", &i);
    fflush(stdout);
    while (i)
    	;
    return 0;
}

在Linux 0.11下跑跑看:

![2019-07-11 13-07-36屏幕截图](lab_7/2019-07-11 13-07-36屏幕截图.png)

打印出了i这个变量的逻辑地址,然后就陷入死循环了。接下来我们要做的就是通过Bochs调试器修改i的值,让循环能够退出,也就是将i修改为0。

地址翻译过程

由于Bochs中我们看到的都是物理地址,我们需要通过i的逻辑地址计算出物理地址。我们先来总结一下通过逻辑地址找到真实物理地址的过程:

由于分段机制的存在,我们看到的这个逻辑地址(0x00003004)只是段内偏移而已,要找到真实的虚拟地址,我们还需要根据段寄存器找到段基址。

具体的操作过程如下:

  1. dsfs等段寄存器中保存的是段基址在LDT(局部描述符表)中的偏移,也就是说LDT[ds]就是真正的段基址,那么问题就是如何找到LDT的基址?
  2. ldtr寄存器保存的是当前进程的LDT基址在GDT(全局描述符表)中的偏移,也就是说GDT[ldtr]中就是LDT的基址,那么如何找到GDT的基址呢?gdtr寄存器中保存的就是GDT的基址。

综上说述,用一种比较好理解的方式来说就是 真正的虚拟地址=gdtr[ldtr][ds] + 段内偏移。接下来就实际操作一下来找到i的真正虚拟地址。

计算虚拟地址

首先按照实验指导书上写的一通操作将Bochs停在while循环里面:

深度截图_选择区域_20190711132339

看到,“刚好”停在判断语句上。另外我们还注意到用到的段基址寄存器是ds。接下来我们用sreg命令查看一下各个寄存器的值:

深度截图_选择区域_20190711132557

观察一下我们所关心的几个寄存器的值:ds寄存器的值是0x0017ldtr的值是0x0068gdtr的值是0x00005cb8。也就是说GDT表的基址是0x00005cb8,而LDT表的基址放在GDT表中的偏移为0x0068=0000000001101000(二进制)。由于段选择子(我们提到的段寄存器包括dsldtr都是段选择子)有自己的特定格式,这不是真正的偏移量,那么真正的偏移是多少呢?接下来就要提到段选择子的格式了:

img

只有前面的13位才是真正的索引号,也就是偏移量。RPL是请求特权级,当访问一个段时,处理器要检查RPL和CPL(放在cs的位0和位1中,用来表示当前代码的特权级),即使程序有足够的特权级(CPL)来访问一个段,但如果RPL(如放在ds中,表示请求数据段)的特权级不足,则仍然不能访问,即如果RPL的数值大于CPL(数值越大,权限越小),则用RPL的值覆盖CPL的值。而段选择子中的TI是表指示标记,如果TI=0,则表示段描述符(段的详细信息)在GDT(全局描述符表)中,即去GDT中去查;而TI=1,则去LDT(局部描述符表)中去查。那么真正的偏移量就应该是1101(二进制)=13(十进制)。也就是说LDT的基址是GDT表中的第14项(下标从0开始)。

我们已经知道了GDT表的基址是0x00005cb8,LDT表的基址在GDT表中,偏移量为13。接下来查看一下下标为13处的值:使用xp /2w 0x00005cb8+13*8查看以8字节单位,下标为13处的值:

深度截图_选择区域_20190712125327

可以看到,确实和ldtr所在行中dldh的值相同。这就是LDT的物理地址,但是我们还需要根据特定的格式将这两个数字组合起来。接下来我们看看段描述符的格式:

img

这里,低32位就是dl=0xc2d00068,高32位就是dh=0x000082f9。根据描述符的格式,将段基址组合起来得到0x00f9c2d0。这就是LDT的真正物理地址了。

有了LDT的基址,又有了ds=0x0017这个偏移,接下来就能得到i的真正虚拟地址了。ds是个段选择子,根据段选择子的格式,我们可以算出ds对应的偏移为0x0017=0000000000010111,其中RPL=11,索引为10(二进制)=2(十进制),表示查找的是LDT中第三个段描述符(从0开始编号)。接下来查看一下LDT中的第三项:

深度截图_选择区域_20190712130623

结果是0x00003fff0x10c0f300。同样,我们按照上面的段描述符的格式将其组合起来得到0x10000000,这就是i的段基址了。**将它和段偏移0x3004组合起来得到0x10003004,这就是i的真正虚拟地址了。**用calc ds:0x3004可以验证这个结果:

深度截图_选择区域_20190712130955

计算物理地址

有了虚拟地址,接下来就是查询页表得到真正的物理地址了。在Linux 0.11中采用的是一个二级页表的结构。将线性地址分为页目录号、页表号和页内偏移,图示如下:

深度截图_选择区域_20190712131304

它们分别对应了32位线性地址的10位+10位+12位。所以,虚拟地址0x10003004对应的页目录号是64,页表号为3,页内偏移是4。

在IA-32下,页目录表的基址有CR3寄存器给出,creg命令能够查看:

深度截图_选择区域_20190712131514

CR3=0x00000000,说明页目录表的基址为0。页目录表中以4字节为单位,查看页目录表中下标为64的位置处的值:

深度截图_选择区域_20190712131741

看到值为0x00faa027。页表的基址就隐藏在这4字节的数字中。这32位中前20位为物理地址,后面是一些属性信息。因此,0x00faa027所代表的物理地址就是0x00faa,也即页表的基地址为0x00faa000。接下来从这个位置查找页表中下标为3的页表项:

深度截图_选择区域_20190712132135

说明真实的页框的基址就是0x00fa7,后面的是一些属性信息。我们把页框的基址和业内偏移0x004组合到一起,得到0x00fa7004,这就是i的物理地址了。

通过指导书上的命令来验证:

深度截图_选择区域_20190712132422

完全正确!

接下来通过直接修改内存来改变i的值,使其值为0来退出循环。命令是setpmem 0x00fa4004 4 0,表示从0x00fa7004地址开始的4个字节都设为0。然后再用“c”命令继续Bochs的运行,可以看到test退出了,说明i的修改成功了,此项实验结束。

基于共享内存的生产者消费者程序

主要是用shmget()shmat()这几个函数,使用方法可以查阅APUE或者百度。

生产者进程如下:

#include <stdio.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>

#define BUFSIZE 10
#define MAX_NUM 500

// producer
int main() 
{
    sem_t *empty;
    sem_t *full;
    sem_t *mutex;
    int shmid;
    int *p;
    int i;

    sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex");

    empty = sem_open("empty", O_CREAT | O_EXCL, S_IRWXU, BUFSIZE);
    if (empty == SEM_FAILED)
    {
        fprintf(stderr, "Error when create semaphore empty\n");
        exit(1);
    }

    full = sem_open("full", O_CREAT | O_EXCL, S_IRWXU, 0);
    if (full == SEM_FAILED)
    {
        fprintf(stderr, "Error when create semaphore full\n");
        exit(1);
    }
    mutex = sem_open("mutex", O_CREAT | O_EXCL, S_IRWXU, 1);
    if (mutex == SEM_FAILED)
    {
        fprintf(stderr, "Error when create semaphore mutex\n");
        exit(1);
    }
    printf("Create semphore OK!\n");

    if((shmid = shmget(1024, MAX_NUM * sizeof(int), IPC_CREAT | 0777)) < 0)
    {
        fprintf(stderr, "shmget error\n");
        exit(1);
    }

    if((p = (int *)shmat(shmid, NULL, SHM_EXEC)) == (void *)-1)
    {
        fprintf(stderr, "shmat error\n");
        exit(1);
    }

    for (i = 0; i < MAX_NUM; ++i)
    {
        sem_wait(empty);
        sem_wait(mutex);
        *(p + i % BUFSIZE) = i;
        printf("add %d to buffer\n", i);
        sem_post(mutex);
        sem_post(full);
    }

    sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex");

    return 0;
}

消费者进程如下:

#include <stdio.h>
#include <semaphore.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>

#define BUFSIZE 10

#define MAX_NUM 500

sem_t* Sem_open(const char *name, unsigned int value)
{
    sem_t *sem = sem_open(name, value);
    if (sem == SEM_FAILED)
    {
        fprintf(stderr, "Error when create semaphore %s\n", name);
        exit(1);
    }
    return sem;
}

int main()
{
    int i;
    int shmid;
    sem_t *empty;
    sem_t *full;
    sem_t *mutex;
    int *p;
    int data;

    empty = Sem_open("empty", 0);  // 使用现有信号量只需指定名字和flag参数的0值
    full = Sem_open("full", 0);
    mutex = Sem_open("mutex", 0);

    printf("Semaphore Open Sucess!\n");
    fflush(stdout);

    shmid = shmget(1024, (BUFSIZE) * sizeof(int), IPC_CREAT | 0777);

    if (shmid == -1)
    {
        fprintf(stderr, "shmget Error!\n");
        exit(1);
    }

    p = (int *)shmat(shmid, NULL, SHM_EXEC);
    for (i = 0; i < MAX_NUM; ++i)
    {
        sem_wait(full);
        sem_wait(mutex);
        data = *(p + i % BUFSIZE);
        printf("%d: %d\n", getpid(), data);
        fflush(stdout);
        sem_post(mutex);
        sem_post(empty);
    }
    return 0;
}

特别提醒:没有父子关系的进程之间进行共享内存,shmget()的第一个参数key不要用IPC_PRIVATE,否则无法共享。用什么数字可视心情而定,只要确保两个进程用的是同一个值即可。

在Linux0.11下实现共享内存

在整个实现过程中存在着两个问题需要解决。

首先,要获得物理地址非常容易,直接调用get_free_page()即可。但是如何在一个进程中获取一个空闲的虚拟地址,然后在页表中将虚拟地址和物理地址联系起来呢?实验指导书中提到,使用put_page()可以将建立虚拟地址和物理地址的映射,那么就剩下如何获得一个虚拟地址了。仔细查看注释中的图13-6,进程的PCB中保存着进程所占用的所有内存空间的信息,比如代码段起始位置,代码段结束位置,栈开始位置等等。为了获得一个虚拟地址,我们可以想想当调用malloc()动态分配内存空间时是如何获取虚拟地址的。我们会发现,malloc()获得的虚拟地址是通过递增brk来分配一个虚拟地址的。也就是说,brk维持了现在地址空间底端已经使用的虚拟地址,我们也只需要递增brk即可获得一块空闲的虚拟地址。另外需要注意的是,图中显示的那些地址比如start_cdoeend_code等等都是段内偏移,因此需要先通过get_base()获取段基址,加上段基址后才是真正的虚拟地址。

第二个问题我们先看看实现共享内存的代码:

shm.h如下:

#ifndef _SHM_H_
#define _SHM_H_

#define SHM_SIZE 20

typedef struct
{
    unsigned int key;
    unsigned int size;
    unsigned long page;  // address
}shm_t;

#endif

shm.c如下:

#include <shm.h>
#include <unistd.h>
#include <errno.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>

static shm_t shm_list[SHM_SIZE] = {0};

int sys_shmget(unsigned int key, size_t size)
{
    int i;
    void *page;
    if (size > PAGE_SIZE)
        return -EINVAL;
    page = get_free_page();  // get an empty physic page.
    // printk("get free page in %p\n", page);
    if (!page)
        return -ENOMEM;
    for (i = 0; i < SHM_SIZE; ++i)
        if (shm_list[i].key == key)
            return i;
    // printk("create new share memory space.\n");
    for (i = 0; i < SHM_SIZE; ++i)
    {
        if (shm_list[i].key == 0)  // find an empty slot.
        {
            shm_list[i].key = key;
            shm_list[i].page = page;
            shm_list[i].size = size;
            return i;
        }
    }
    // printk("no empty slot\n");
    return -1;
}

void* sys_shmat(int shmid)
{
    int i;
    void *addr;
    if (0 < shmid || shmid > SHM_SIZE)
        return -EINVAL;
    addr = current->brk + get_base(current->ldt[1]);  // we can use this virtual address
    current->brk += PAGE_SIZE;
    // printk("get an empty virtual address in %p\n", addr);
    if (shm_list[shmid].key != 0)
    {
        put_page(shm_list[shmid].page, addr);
        incr_mem_map(shm_list[shmid].page);   // 重点
        return current->brk - PAGE_SIZE;
    }
    // printk("this share memory is not exits.\n");

    return -EINVAL;
}

实现很简单,但是里面还存在一个问题。我们现在的生产消费者程序有两个进程,一个producer,一个consumer。他们之间共享一块物理内存。当producer退出时,操作系统会自动调用free_page()来回收这个进程使用的所有内存页,那么也会回收这块共享内存。所谓回收也就是在全局的mem_map表中将该物理内存对应的位置的值减一,如果为0了就真正的回收内存,标记为可用。当我们调用get_free_page()时只是在mem_map中将对应物理内存设置为1,那么producer退出时会减小至0,该内存已经被标记为可用了。然后,consumer退出,操作系统同样会尝试取释放这块共享内存,但是这块内存已经被释放了,因此会出现trying to free free page,接着就会宕机。因此,当我们调用shmat时我们需要在mem_map中将对应项加一,这样可以避免宕机。但是可能会出现内存泄漏?

因此,我们需要在memory.c中增加一个对mem_map进行加一操作的函数,如下:

void incr_mem_map(unsigned long addr)
{
        
        mem_map[MAP_NR(addr)]++;
}

具体为什么这么实现可以看看《注释》。

接下来就是修改后的consumer.cproducer.c了,实现如下:

producer.c

#define __LIBRARY__

#include <stdio.h>
#include <sem.h>
#include <fcntl.h>
#include <shm.h>
#include <unistd.h>
#include <stdlib.h>

/* for semphare */
_syscall2(sem_t*, sem_open, const char*, name, unsigned int, value);
_syscall1(int, sem_wait, sem_t*, sem);
_syscall1(int, sem_post, sem_t*, sem);
_syscall1(int, sem_unlink, const char*, name);

_syscall1(void*, shmat, int, shmid);
_syscall2(int, shmget, unsigned int, key, size_t, size);

#define BUFSIZE 10
#define MAX_NUM 500

/* producer */
int main() 
{
    sem_t *empty;
    sem_t *full;
    sem_t *mutex;
    int shmid;
    int *p;
    int i;

    sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex");

    empty = sem_open("empty", BUFSIZE);
    full = sem_open("full", 0);
    mutex = sem_open("mutex", 1);

    shmget(1024, MAX_NUM * sizeof(int));
    p = (int *)shmat(shmid);
    
    for (i = 0; i < MAX_NUM; ++i)
    {
        sem_wait(empty);
        sem_wait(mutex);
        *(p + i % BUFSIZE) = i;
        printf("add %d to buffer\n", i);
        fflush(stdout);
        sem_post(mutex);
        sem_post(full);
    }

    /* sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex"); */
    printf("Producer exit\n");
    fflush(stdout);

    return 0;
}

consumer.c

#define __LIBRARY__

#include <stdio.h>
#include <sem.h>
#include <shm.h>
#include <stdlib.h>
#include <unistd.h>

_syscall2(sem_t*, sem_open, const char*, name, unsigned int, value);
_syscall1(int, sem_wait, sem_t*, sem);
_syscall1(int, sem_post, sem_t*, sem);
_syscall1(int, sem_unlink, const char*, name);

_syscall1(void*, shmat, int, shmid);
_syscall2(int, shmget, unsigned int, key, size_t, size);


#define BUFSIZE 10

#define MAX_NUM 500

sem_t* Sem_open(const char *name, unsigned int value)
{
    sem_t *sem = sem_open(name, value);
    if (sem == NULL)
    {
        fprintf(stderr, "Error when create semaphore %s\n", name);
        exit(1);
    }
    return sem;
}

int main()
{
    int i;
    int shmid;
    sem_t *empty;
    sem_t *full;
    sem_t *mutex;
    int *p;
    int data;

    empty = Sem_open("empty", BUFSIZE);
    full = Sem_open("full", 0);
    mutex = Sem_open("mutex", 1);

    shmid = shmget(1024, (BUFSIZE) * sizeof(int));

    p = (int *)shmat(shmid);

    for (i = 0; i < MAX_NUM; ++i)
    {
        sem_wait(full);
        sem_wait(mutex);
        data = *(p + i % BUFSIZE);
        printf("%d: %d\n", getpid(), data);
        fflush(stdout);
        sem_post(mutex);
        sem_post(empty);
    }
    return 0;
}

也没有太大的改动。

另外还有一个注意点,我的这两个程序中没有对信号量进行释放,因为如果在producer中释放了在consumer中就没法访问了。当然,你可以在consumer中释放,但是我觉得这样并不优雅。。所以就没这样做。如果想做到释放,那么可以在信号量的实现中增加计数器,如果计数值为0了才真正释放该信号量,这样就可以比较优雅的在生产消费者程序中释放信号量了。在此我没有实现。