前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过linux0.11源码理解进程的虚拟地址、线性地址、物理地址

通过linux0.11源码理解进程的虚拟地址、线性地址、物理地址

作者头像
theanarkh
发布2019-04-24 11:05:03
1.5K0
发布2019-04-24 11:05:03
举报
文章被收录于专栏:原创分享

进程的地址有三种,分别是虚拟地址(逻辑地址)、线性地址、物理地址。在分析之前先讲一下进程执行的时候,地址的解析过程。在保护模式下,段寄存器保存的是段选择子,当进程被系统选中执行时,会把tss和ldt等信息加载到寄存器中,tss是保存进程上下文的,ldt是保存进程代码和数据段的首地址偏移以及权限等信息的。假设当前执行cs:ip指向的代码,系统根据ldt的值从gdt中选择一个元素,里面保存的是idt结构的首地址。然后根据cs的值选择idt表格中的一项,从而得到代码段的基地址和限长,用基地址加上ip指向的偏移得到一个线性地址,这个线性地址分为三个部分,分别是页目录索引,页表索引,物理地址偏移。然后到页目录吧和页表中找到物理地址基地址,再加线性地址中的偏移部分,得到物理地址。下面我们看看这些内容是怎么设置的,使得执行的时候能正确找到我们想要的地址去执行代码。我们从fork函数开始。到进程被调度执行时所发生的事情。fork函数的具体调用过程之前已经分析过。下面贴一下主要的代码。

代码语言:javascript
复制
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
        long ebx,long ecx,long edx,
        long fs,long es,long ds,
        long eip,long cs,long eflags,long esp,long ss)
{
    struct task_struct *p;
    int i;
    struct file *f;
    // 申请一页存pcb
    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    // 挂载到全局pcb数组
    task[nr] = p;
    // 复制当前进程的数据
    *p = *current;  /* NOTE! this doesn't copy the supervisor stack */
    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;
    p->signal = 0;
    p->alarm = 0;
    p->leader = 0;      /* process leadership doesn't inherit */
    p->utime = p->stime = 0;
    p->cutime = p->cstime = 0;
    // 当前时间
    p->start_time = jiffies;
    p->tss.back_link = 0;
    // 页末
    p->tss.esp0 = PAGE_SIZE + (long) p;
    p->tss.ss0 = 0x10;
    // 调用fork时压入栈的ip,子进程创建完成会从这开始执行,即if (__res >= 0) 
    p->tss.eip = eip;
    p->tss.eflags = eflags;
    // 子进程从fork返回的是0,eax会赋值给__res
    p->tss.eax = 0;
    p->tss.ecx = ecx;
    p->tss.edx = edx;
    p->tss.ebx = ebx;
    p->tss.esp = esp;
    p->tss.ebp = ebp;
    p->tss.esi = esi;
    p->tss.edi = edi;
    // 段选择子是16位
    p->tss.es = es & 0xffff;
    p->tss.cs = cs & 0xffff;
    p->tss.ss = ss & 0xffff;
    p->tss.ds = ds & 0xffff;
    p->tss.fs = fs & 0xffff;
    p->tss.gs = gs & 0xffff;
    /*
        计算第nr进程在GDT中关于LDT的索引,切换任务的时候,
        这个索引会被加载到ldt寄存器,cpu会自动根据ldt的值,把
        GDT中相应位置的段描述符加载到ldt寄存器(共16+32+16位)
    */
    p->tss.ldt = _LDT(nr); 
    p->tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    /*
    设置线性地址范围,挂载线性地址首地址和限长到idt,赋值页目录项和页表
    执行进程的时候,tss选择子被加载到tss寄存器,然后把tss里的上下文
    也加载到对应的寄存器,比如cr3,ldt选择子。tss信息中的idt索引首先从gdt找到进程idt
    结构体数据的首地址,然后根据当前段的属性,比如代码段,
    则从cs中取得选择子,系统从idt表中取得进程线性空间
    的首地址、限长、权限等信息。用线性地址的首地址加上ip
    中的偏移,得到线性地址,然后再通过页目录和页表得到物理
    地址,物理地址还没有分配则进行缺页异常等处理。
    */
    if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
    // 父子进程都有同样的文件描述符,file结构体加一
    for (i=0; i<NR_OPEN;i++)
        if (f=p->filp[i])
            f->f_count++;
    // inode节点加一
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
    /*
        挂载tss和idt地址到gdt,nr << 1即乘以2,这里算出的是第nr个进程距离第一个tss描述符地址的偏移,
        单位是8个字节,即选择描述符大小
    */
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING;    /* do this last, just in case */
    return last_pid;
}

fork函数收到分配一页的物理内存保存PCB结构,然后把父进程的信息复制过来,再修改某些字段。接着计算一个在全局描述符GDT中的一个索引,这个索引是ldt选择子。后面会讲到。然后计算进程的代码和数据的线性地址首地址和限长,写到ldt的描述符中。接着复制页表,但是不分配物理地址。最后把tss结构和ldt结构挂载到GDT中。fork函数就完成了。下面看看选择子和描述符的格式。

在这里插入图片描述 下面选择子的计算

代码语言:javascript
复制
/*
 * Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall
 * 4-TSS0, 5-LDT0, 6-TSS1 etc ...
 */
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
// 第一个tss选择子的偏移是4<<3,4乘以8,等于32,即从GDT的偏移为32开始算,第一个进程的n是0,tss是32
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// 第一个ldt选择子的偏移是5<<3,5乘以8,等于40,即从GDT的偏移为40开始算,第一个进程的n是0,ldt是40
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))

下面是段描述符的设置。

代码语言:javascript
复制
#define PAGE_ALIGN(n) (((n)+0xfff)&0xfffff000)
/*
    段描述符的地3,4,5,7四个字节是保存基地址
    把edx的两个字节保存在addr+2,即第3,4位
    edx右移16位,把低位给addr的第四个字节
    把高位给addr的第七个字节
*/
#define _set_base(addr,base) \
__asm__("movw %%dx,%0\n\t" \
    "rorl $16,%%edx\n\t" \
    "movb %%dl,%1\n\t" \
    "movb %%dh,%2" \
    // 四个输入
    ::"m" (*((addr)+2)), \
      "m" (*((addr)+4)), \
      "m" (*((addr)+7)), \
      "d" (base) \
    :"dx")
/*
    段描述符的地第1,2字节和16-19位保存段限长
    把dx的两个字节给addr的第1,2个字节,edx右移16位
    把addr的第六个字节赋值给dh,
    把dh的前四个比特清0,再把dh高四位复制到dl高四位,
    dl的低四位和高四位组成新的比特顺序,把dl写回addr的第六个字节
*/
#define _set_limit(addr,limit) \
__asm__("movw %%dx,%0\n\t" \
    "rorl $16,%%edx\n\t" \
    "movb %1,%%dh\n\t" \
    "andb $0xf0,%%dh\n\t" \
    "orb %%dh,%%dl\n\t" \
    "movb %%dl,%1" \
    // 三个输入
    ::"m" (*(addr)), \
      "m" (*((addr)+6)), \
      "d" (limit) \
    :"dx")

#define set_base(ldt,base) _set_base( ((char *)&(ldt)) , base )
#define set_limit(ldt,limit) _set_limit( ((char *)&(ldt)) , (limit-1)>>12 )

// 把三个字节逐个复制到__base
#define _get_base(addr) ({\
unsigned long __base; \
__asm__("movb %3,%%dh\n\t" \
    "movb %2,%%dl\n\t" \
    // edx=edx左移16位
    "shll $16,%%edx\n\t" \
    "movw %1,%%dx" \
    // edx寄存器的值写到__base
    :"=d" (__base) \
    // 输入
    :"m" (*((addr)+2)), \
     "m" (*((addr)+4)), \
     "m" (*((addr)+7))); \
__base;})

#define get_base(ldt) _get_base( ((char *)&(ldt)) )

// 加载段限长,把segment对应的段描述符中的段界限字段加载到limit
#define get_limit(segment) ({ \
unsigned long __limit; \
__asm__("lsll %1,%0\n\tincl %0":"=r" (__limit):"r" (segment)); \
__limit;})

设置完后,在进程被系统选中执行时,下面首先看看进程切换的代码。

代码语言:javascript
复制
#define switch_to(n) {\
struct {long a,b;} __tmp; \
// ecx是第n个进程对应的pcb首地址,判断切换的下一个进程是不是就是当前执行的进程,是就不需要切换了
__asm__("cmpl %%ecx,_current\n\t" \
    "je 1f\n\t" \
    // 把第n个进程的tss选择子复制到__tmp.b
    "movw %%dx,%1\n\t" \
    // 更新current变量,使current变量执行ecx,ecx指向task[n]
    "xchgl %%ecx,_current\n\t" \
    // ljmp 跟一个tss选择子实现进程切换
    "ljmp %0\n\t" \
    // 忽略
    "cmpl %%ecx,_last_task_used_math\n\t" \
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

ljmp tss描述符后,系统会加载第n个进程的tss选择子到ltr(保存tss选择子和首地址偏移信息的寄存器),根据选择子从GDT拿到tss的段选择符,然后找到tss的内容,再把某些内容加载到相应寄存器,比如idt信息。最后根据tss中的cs和ip执行进程。这就是文章开头的过程。这就是linux0.11版本中进程地址管理的实现。下面是fork后的结构图。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档