ucore的内存布局(一)

从lab2开始,ucore开启了基于段页式内存地址的转换机制,使得ucore的内存布局不同于lab1,对于这部分内容我打算分两篇文章来讲述,分别对应lab1和lab2。

lab1时期ucore的(物理/逻辑地址)内存布局

ucore如何被加载进内核

完成系统初始化和检测工作后,BIOS会加载磁盘的第一个扇区,称为主引导扇区,然后执行其中的代码。这部分代码就是bootloader,它会开启32位的保护模式,接着将内核镜像加载进内存,再跳转到内核第一条指令处执行内核程序。

qemu用来模拟硬件环境,加载硬盘镜像文件ucore.img中的内容并执行,这个过程就类似于计算机启动加载磁盘上的操作系统。而这个ucore.img就是由bootloader和kernel构成,在我的另一篇文章中也有介绍,如下图所示。

➜ readelf -S bootblock.o
共有 9 个节头,从偏移量 0x1390 开始:
节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00007c00 000074 000184 00 WAX  0   0  4
  [ 2] .eh_frame         PROGBITS        00007d84 0001f8 000068 00   A  0   0  4
  [ 3] .stab             PROGBITS        00000000 000260 000798 0c      4   0  4
  ...

很显然bootloader将被载入到物理内存0x7c00处,正如前面第一幅图橘黄色所示。bootloader开启了保护模式后,寻址空间变为32位没有其他变化,接着bootloader开始加载kernel。具体加载在何处由一下代码决定。

// 原型
/* *
 * readseg - read @count bytes at @offset from kernel into virtual address @va,
 * might copy more than asked.
 * */
static void readseg(uintptr_t va, uint32_t count, uint32_t offset);

// 实际调用
// kernel是elf格式的,先读取elf header
// ELFHDR为0x10000,SECTSIZE为512
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);

注意readseg的入参va是指虚拟地址,而此时的计算机其实还是基于段机制管理内存,并且线性地址等于物理地址(段基置为0),所以va就是物理地址,故kernel的elf头部加载地址为0x10000,代码段被加载在物理地址0x100000。

➜ readelf -S kernel 
共有 11 个节头,从偏移量 0x122c0 开始:
节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00100000 001000 0035d7 00  AX  0   0  1
  [ 2] .rodata           PROGBITS        001035e0 0045e0 00090c 00   A  0   0 32
  [ 3] .stab             PROGBITS        00103eec 004eec 007a95 0c   A  4   0  4
  [ 4] .stabstr          STRTAB          0010b981 00c981 00206a 00   A  0   0  1

这里需要说明的是 Addr这列的地址是指虚拟地址,而非物理地址,所以在调用readseg时发现有个 & 0xFFFFFF 操作,这是为了调整kernel的各段加载在物理内存中的位置。前面也说了,现在的虚拟地址和物理地址是一一对应,不进行调整的话,这些段就被加载到ph->p_va所指的物理地址空间,那么就会产生一个问题,如果计算机本身的物理空间有限,比如只有一个2g的内存条,但程序被要求加载进入3G的地址空间,很明显这是无法满足的。单从本次看Addr0x100000所指地址为1M不算大,现在的计算机肯定都能满足,但是在lab2注意观察会发现kernel的.text的Addr变为0xc0100000(3G),这就会碰到前面提到的问题。

kernel.ld 分析

到这里其实已经差不多讲完了,后面再补充一些为什么kernel的.text段的起始位置为0x100000,我们来看下kernel的链接脚本 kernel.ld

/* Simple linker script for the JOS kernel.
   See the GNU ld 'info' manual ("info ld") to learn the syntax. */

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(kern_init) /*执行入口*/

SECTIONS {
    /* 将内核加载在下面这个地址处: "." 表示当前地址 */
    . = 0x100000;
    
    /*.text段(segment)包含的段(section)*/
    .text : {
        *(.text .stub .text.* .gnu.linkonce.t.*)
    }

    PROVIDE(etext = .);    /* 定义'etext'符号,它的值为此处的地址 */
    
    /* 同.text* /
    .rodata : {
        *(.rodata .rodata.* .gnu.linkonce.r.*)
    }

    /* 包含一些debug信息 */
    .stab : {
        PROVIDE(__STAB_BEGIN__ = .);
        *(.stab);
        PROVIDE(__STAB_END__ = .);
        BYTE(0)        /* Force the linker to allocate space
                   for this section */
    }

    .stabstr : {
        PROVIDE(__STABSTR_BEGIN__ = .);
        *(.stabstr);
        PROVIDE(__STABSTR_END__ = .);
        BYTE(0)        /* Force the linker to allocate space
                   for this section */
    }

    /* 调整地址边界以4k对齐(一个page页面的大小),从而是data段加载进下一个页 */
    . = ALIGN(0x1000);

    /* 数据段 */
    .data : {
        *(.data)
    }
    
    /* 同 etext */
    PROVIDE(edata = .);
    
    .bss : {
        *(.bss)
    }

    /* 同etext */
    PROVIDE(end = .);
    
    /* 舍弃的一些段(section)*/
    /DISCARD/ : {
        *(.eh_frame .note.GNU-stack)
    }
}

见kernel.ld的第10行可知内核应该加载在内存地址0x100000处。我们还看到了很多PROVIDE()命令,它相当于声明了一个表示当前地址的变量,在ucore的其他文件中可以通过extern 来声明使用

你可能感兴趣的:(c,操作系统)