hurlex <十> 虚拟内存管理的实现

2014-09-15 posted in [hurlex开发文档]

这章将详细研讨虚拟内存管理的实现。

上一章谈到,虚拟的页面每页占据4KB,按页为单位进行管理。物理内存也被分页管理,按照4KB分为一个个物理页框。虚拟地址到物理地址通过由页目录和页表组成的二级页表映射,页目录的地址放置在CR3寄存器里。

至此,我们彻底揭开了x86下32位寻址的面纱,下图描述了地址转换的完整过程。

段页式转换

因为我们使用了Intel平坦模式的内存模型,所以之前的分段机制是被“绕过去”的,所以分页的管理就成了内存管理的核心了。首先是内核自身地址的映射,Linux采用的方案是把内核映射到线性地址空间3G以上,而应用程序占据线性地址空间0-3G的位置。我们的内核采取和Linux内核一样的映射,把物理地址0从虚拟地址0xC0000000(3G)处开始往上映射,因为我们只管理最多512MB的内存,所以3G-4G之间能完全的映射全部的物理地址。采取这个映射后,物理地址和内核虚拟地址满足以下关系:

物理地址 + 0xC0000000 = 内核虚拟地址

但是采用这个设计的话会给已有的代码带来什么麻烦呢?

我们先引入VMA(Virtual Memory Address)和LMA(Load MemoryAddress)这两个概念。其中VMA是链接器生成可执行文件时的偏移计算地址,而LMA是区段所载入内存的实际地址。通常情况下的VMA是等于LMA的。使用以下命令可以查看内核文件的区段信息:

  1. objdump -h hx_kernel

输出大概是这个样子:

  1. hx_kernel file format elf32-i386
  2. section:
  3. Idx Name Size VMA LMA File off Algn
  4. 0 .text 00003000 00100000 00100000 00000080 2**4
  5. CONTENTS, ALLOC, LOAD, READONLY, CODE
  6. 1 .data 00001000 00103000 00103000 00003080 2**2
  7. CONTENTS, ALLOC, LOAD, DATA
  8. 2 .bss 00089c64 00104000 00104000 00004080 2**5
  9. ALLOC
  10. 3 .stab 0000539c 0018dc64 0018dc64 0008dce4 2**2
  11. CONTENTS, ALLOC, LOAD, READONLY, DATA
  12. 4 .stabstr 00002000 00193000 00193000 00093080 2**0
  13. CONTENTS, ALLOC, LOAD, READONLY, DATA

从上面的结果中能看到目前区段的加载地址和虚拟地址都是一样的。按照上面的设计,我们需要修改链接器脚本中各个段的起始位置。但是简单的把代码段的起始位置设为0xC0100000的话内核一运行就出错。为什么呢?因为GRUB是从1MB处加载内核的,而链接器是以0xC0100000这个参考地址进行地址重定位的。此时尚未开启虚拟页面映射,运行涉及到寻址的代码肯定就会出错。怎么办呢?看起来像是一个无解的死循环了。如果GRUB在加载内核之前就能设定好虚拟地址的映射再执行内核多好,或者有一段程序和数据按照0x100000的地址进行重定位,能帮助我们设置好一个临时的页表,再跳转到内核入口函数多好。前者貌似不可能实现,那后者呢?答案是肯定的,我们就采用这个方案。

GCC提供了这样的扩展机制:允许程序员指定某个函数或者某个变量所存储的区段。同时ld的链接脚本又可以自由定制,所以这个无解的问题就有了解决方案。用于设置这个临时页表和函数我们指定它存储在.init段,只需要指定该段从0x100000地址开始,其他的.text和.data等段按照0xC0100000作为起始地址即可。当然这里还有要注意的细节,具体在下面的新链接脚本中可以看。因为代码变化比较大,所以贴出全部链接器脚本如下:

  1. ENTRY(start)
  2. SECTIONS
  3. {
  4. PROVIDE( kern_start = 0xC0100000);
  5. . = 0x100000;
  6. .init.text :
  7. {
  8. *(.init.text)
  9. . = ALIGN(4096);
  10. }
  11. .init.data :
  12. {
  13. *(.init.data)
  14. . = ALIGN(4096);
  15. }
  16. . += 0xC0000000;
  17. .text : AT(ADDR(.text) - 0xC0000000)
  18. {
  19. *(.text)
  20. . = ALIGN(4096);
  21. }
  22. .data : AT(ADDR(.data) - 0xC0000000)
  23. {
  24. *(.data)
  25. *(.rodata)
  26. . = ALIGN(4096);
  27. }
  28. .bss : AT(ADDR(.bss) - 0xC0000000)
  29. {
  30. *(.bss)
  31. . = ALIGN(4096);
  32. }
  33. .stab : AT(ADDR(.stab) - 0xC0000000)
  34. {
  35. *(.stab)
  36. . = ALIGN(4096);
  37. }
  38. .stabstr : AT(ADDR(.stabstr) - 0xC0000000)
  39. {
  40. *(.stabstr)
  41. . = ALIGN(4096);
  42. }
  43. PROVIDE( kern_end = . );
  44. /DISCARD/ : { *(.comment) *(.eh_frame) }
  45. }

链接脚本更新之后,之前一些代码也需要做出改动。首先要修改的是入口函数。因为修改的地方略多,所以贴出除声明外完整代码:

  1. ... ...
  2. [BITS 32] ; 所有代码以 32-bit 的方式编译
  3. section .init.text ; 临时代码段从这里开始
  4. ; 在代码段的起始位置设置符合 Multiboot 规范的标记
  5. dd MBOOT_HEADER_MAGIC ; GRUB 会通过这个魔数判断该映像是否支持
  6. dd MBOOT_HEADER_FLAGS ; GRUB 的一些加载时选项,其详细注释在定义处
  7. dd MBOOT_CHECKSUM ; 检测数值,其含义在定义处
  8. [GLOBAL start] ; 内核代码入口,此处提供该声明给 ld 链接器
  9. [GLOBAL mboot_ptr_tmp] ; 全局的 struct multiboot * 变量
  10. [EXTERN kern_entry] ; 声明内核 C 代码的入口函数
  11. start:
  12. cli ; 此时还没有设置好保护模式的中断处理
  13. ; 所以必须关闭中断
  14. mov [mboot_ptr_tmp], ebx ; ebx 中存储的指针存入 glb_mboot_ptr 变量
  15. mov esp, STACK_TOP ; 设置内核栈地址,按照 multiboot 规范
  16. and esp, 0FFFFFFF0H ; 栈地址按照 16 字节对齐
  17. mov ebp, 0 ; 帧指针修改为 0
  18. call kern_entry ; 调用内核入口函数
  19. ;-----------------------------------------------------------------------------
  20. section .init.data ; 开启分页前临时的数据段
  21. stack: times 1024 db 0 ; 这里作为临时内核栈
  22. STACK_TOP equ - -stack-1 ; 内核栈顶,- 符指代是当前地址
  23. mboot_ptr_tmp: dd 0 ; 全局的 multiboot 结构体指针
  24. ;-----------------------------------------------------------------------------

主要的修改是第5行的代码所在段声明和第29行的数据所在段声明,因为此处代码和数据是在参考0x100000(1MB)编址的。所以在进入分页后需要更换新的内核栈和新的multiboot结构体指针。除此之外,仍就需要指定kern_entry函数所在区段为.init.text段,并且在该函数中建立临时页表并跳转到高虚拟地址处的kern_init函数正式执行,代码如下:

  1. #include "console.h"
  2. #include "string.h"
  3. #include "debug.h"
  4. #include "gdt.h"
  5. #include "idt.h"
  6. #include "timer.h"
  7. #include "pmm.h"
  8. #include "vmm.h"
  9. // 内核初始化函数
  10. void kern_init();
  11. // 开启分页机制之后的 Multiboot 数据指针
  12. multiboot_t *glb_mboot_ptr;
  13. // 开启分页机制之后的内核栈
  14. char kern_stack[STACK_SIZE];
  15. // 内核使用的临时页表和页目录
  16. // 该地址必须是页对齐的地址,内存 0-640KB 肯定是空闲的
  17. __attribute__((section(".init.data"))) pgd_t *pgd_tmp = (pgd_t *)0x1000;
  18. __attribute__((section(".init.data"))) pgd_t *pte_low = (pgd_t *)0x2000;
  19. __attribute__((section(".init.data"))) pgd_t *pte_hign = (pgd_t *)0x3000;
  20. // 内核入口函数
  21. __attribute__((section(".init.text"))) void kern_entry()
  22. {
  23. pgd_tmp[0] = (uint32_t)pte_low | PAGE_PRESENT | PAGE_WRITE;
  24. pgd_tmp[PGD_INDEX(PAGE_OFFSET)] = (uint32_t)pte_hign | PAGE_PRESENT | PAGE_WRITE;
  25. // 映射内核虚拟地址 4MB 到物理地址的前 4MB
  26. int i;
  27. for (i = 0; i < 1024; i++) {
  28. pte_low[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE;
  29. }
  30. // 映射 0x00000000-0x00400000 的物理地址到虚拟地址 0xC0000000-0xC0400000
  31. for (i = 0; i < 1024; i++) {
  32. pte_hign[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE;
  33. }
  34. // 设置临时页表
  35. asm volatile ("mov %0, %%cr3" : : "r" (pgd_tmp));
  36. uint32_t cr0;
  37. // 启用分页,将 cr0 寄存器的分页位置为 1 就好
  38. asm volatile ("mov %%cr0, %0" : "=r" (cr0));
  39. cr0 |= 0x80000000;
  40. asm volatile ("mov %0, %%cr0" : : "r" (cr0));
  41. // 切换内核栈
  42. uint32_t kern_stack_top = ((uint32_t)kern_stack + STACK_SIZE) & 0xFFFFFFF0;
  43. asm volatile ("mov %0, %%esp\n\t"
  44. "xor %%ebp, %%ebp" : : "r" (kern_stack_top));
  45. // 更新全局 multiboot_t 指针
  46. glb_mboot_ptr = mboot_ptr_tmp + PAGE_OFFSET;
  47. // 调用内核初始化函数
  48. kern_init();
  49. }
  50. void kern_init()
  51. {
  52. init_debug();
  53. init_gdt();
  54. init_idt();
  55. console_clear();
  56. printk_color(rc_black, rc_green, "Hello, OS kernel!\n\n");
  57. init_timer(200);
  58. // 开启中断
  59. // asm volatile ("sti");
  60. printk("kernel in memory start: 0x%08X\n", kern_start);
  61. printk("kernel in memory end: 0x%08X\n", kern_end);
  62. printk("kernel in memory used: %d KB\n\n", (kern_end - kern_start) / 1024);
  63. show_memory_map();
  64. init_pmm();
  65. printk_color(rc_black, rc_red, "\nThe Count of Physical Memory Page is: %u\n\n", phy_page_count);
  66. uint32_t allc_addr = NULL;
  67. printk_color(rc_black, rc_light_brown, "Test Physical Memory Alloc :\n");
  68. allc_addr = pmm_alloc_page();
  69. printk_color(rc_black, rc_light_brown, "Alloc Physical Addr: 0x%08X\n", allc_addr);
  70. allc_addr = pmm_alloc_page();
  71. printk_color(rc_black, rc_light_brown, "Alloc Physical Addr: 0x%08X\n", allc_addr);
  72. allc_addr = pmm_alloc_page();
  73. printk_color(rc_black, rc_light_brown, "Alloc Physical Addr: 0x%08X\n", allc_addr);
  74. allc_addr = pmm_alloc_page();
  75. printk_color(rc_black, rc_light_brown, "Alloc Physical Addr: 0x%08X\n", allc_addr);
  76. while (1) {
  77. asm volatile ("hlt");
  78. }
  79. }

代码中的 attribute((section(“.init.data”)))是GCC编译器的扩展功能,用来指定变量或者函数的存储区段。我们使用了1MB以下地址空间中的12KB来暂时放置临时页表。除此之外,入口函数中除了映射0xC0000000(3G)开始的4MB地址到物理内存0-4MB之外,我们依旧把虚拟地址的0-4MB映射到了物理地址的同样位置。为什么呢?因为在代码48-50行一旦将CR0寄存器最高位置为1的话,CPU立即就会进入分页机制去运行,此时所有的寻址都会按照分页机制的原则去进行,而kern_entry函数本身是按照1MB起始地址生成的虚拟地址,如果不映射低端的虚拟地址的话,kern_entry开启分页之后的代码访问就会出错。而最终离开了这个入口函数,进入内核初始化函数kern_init的时候,已经处于高端虚拟地址的区域。所以在新的页表里不再需要低端的映射也可以正常寻址了。

别忘了要更新multiboot.h的声明:

  1. // 声明全局的 multiboot_t * 指针
  2. // 内核未建立分页机制前暂存的指针
  3. extern multiboot_t *mboot_ptr_tmp;
  4. // 内核页表建立后的指针
  5. extern multiboot_t *glb_mboot_ptr;

另外还需要修改文本模式下显存的起始位置,原先的地址0xB8000此时需要加上偏移地址0xC0000000才可以在分页模式下正常访问到。

  1. ... ...
  2. // VGA 的显示缓冲的起点是 0xB8000
  3. static uint16_t *video_memory = (uint16_t *)(0xB8000 + PAGE_OFFSET);
  4. ... ...

之前的elf_t结构体存储的是低端内存的地址,现在也必须加上页偏移:

  1. ... ...
  2. // 从 multiboot_t 结构获取 ELF 信息
  3. elf_t elf_from_multiboot(multiboot_t *mb)
  4. {
  5. int i;
  6. elf_t elf;
  7. elf_section_header_t *sh = (elf_section_header_t *)mb->addr;
  8. uint32_t shstrtab = sh[mb->shndx].addr;
  9. for (i = 0; i < mb->num; i++) {
  10. const char *name = (const char *)(shstrtab + sh[i].name) + PAGE_OFFSET;
  11. // 在 GRUB 提供的 multiboot 信息中寻找内核 ELF 格式所提取的字符串表和符号表
  12. if (strcmp(name, ".strtab") == 0) {
  13. elf.strtab = (const char *)sh[i].addr + PAGE_OFFSET;
  14. elf.strtabsz = sh[i].size;
  15. }
  16. if (strcmp(name, ".symtab") == 0) {
  17. elf.symtab = (elf_symbol_t *)(sh[i].addr + PAGE_OFFSET);
  18. elf.symtabsz = sh[i].size;
  19. }
  20. }
  21. return elf;
  22. }
  23. ... ...

最后是实现虚拟内存管理的初始化了,这个函数将建立正式的内核页表并进行切换。同时还有进行地址映射和解除映射的函数实现:

  1. #include "idt.h"
  2. #include "string.h"
  3. #include "debug.h"
  4. #include "vmm.h"
  5. #include "pmm.h"
  6. // 内核页目录区域
  7. pgd_t pgd_kern[PGD_SIZE] __attribute__ ((aligned(PAGE_SIZE)));
  8. // 内核页表区域
  9. static pte_t pte_kern[PTE_COUNT][PTE_SIZE] __attribute__ ((aligned(PAGE_SIZE)));
  10. void init_vmm()
  11. {
  12. // 0xC0000000 这个地址在页目录的索引
  13. uint32_t kern_pte_first_idx = PGD_INDEX(PAGE_OFFSET);
  14. uint32_t i, j;
  15. for (i = kern_pte_first_idx, j = 0; i < PTE_COUNT + kern_pte_first_idx; i++, j++) {
  16. // 此处是内核虚拟地址,MMU 需要物理地址,所以减去偏移,下同
  17. pgd_kern[i] = ((uint32_t)pte_kern[j] - PAGE_OFFSET) | PAGE_PRESENT | PAGE_WRITE;
  18. }
  19. uint32_t *pte = (uint32_t *)pte_kern;
  20. // 不映射第 0 页,便于跟踪 NULL 指针
  21. for (i = 1; i < PTE_COUNT * PTE_SIZE; i++) {
  22. pte[i] = (i << 12) | PAGE_PRESENT | PAGE_WRITE;
  23. }
  24. uint32_t pgd_kern_phy_addr = (uint32_t)pgd_kern - PAGE_OFFSET;
  25. // 注册页错误中断的处理函数 ( 14 是页故障的中断号 )
  26. register_interrupt_handler(14, &page_fault);
  27. switch_pgd(pgd_kern_phy_addr);
  28. }
  29. void switch_pgd(uint32_t pd)
  30. {
  31. asm volatile ("mov %0, %%cr3" : : "r" (pd));
  32. }
  33. void map(pgd_t *pgd_now, uint32_t va, uint32_t pa, uint32_t flags)
  34. {
  35. uint32_t pgd_idx = PGD_INDEX(va);
  36. uint32_t pte_idx = PTE_INDEX(va);
  37. pte_t *pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
  38. if (!pte) {
  39. pte = (pte_t *)pmm_alloc_page();
  40. pgd_now[pgd_idx] = (uint32_t)pte | PAGE_PRESENT | PAGE_WRITE;
  41. // 转换到内核线性地址并清 0
  42. pte = (pte_t *)((uint32_t)pte + PAGE_OFFSET);
  43. bzero(pte, PAGE_SIZE);
  44. } else {
  45. // 转换到内核线性地址
  46. pte = (pte_t *)((uint32_t)pte + PAGE_OFFSET);
  47. }
  48. pte[pte_idx] = (pa & PAGE_MASK) | flags;
  49. // 通知 CPU 更新页表缓存
  50. asm volatile ("invlpg (%0)" : : "a" (va));
  51. }
  52. void unmap(pgd_t *pgd_now, uint32_t va)
  53. {
  54. uint32_t pgd_idx = PGD_INDEX(va);
  55. uint32_t pte_idx = PTE_INDEX(va);
  56. pte_t *pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
  57. if (!pte) {
  58. return;
  59. }
  60. // 转换到内核线性地址
  61. pte = (pte_t *)((uint32_t)pte + PAGE_OFFSET);
  62. pte[pte_idx] = 0;
  63. // 通知 CPU 更新页表缓存
  64. asm volatile ("invlpg (%0)" : : "a" (va));
  65. }
  66. uint32_t get_mapping(pgd_t *pgd_now, uint32_t va, uint32_t *pa)
  67. {
  68. uint32_t pgd_idx = PGD_INDEX(va);
  69. uint32_t pte_idx = PTE_INDEX(va);
  70. pte_t *pte = (pte_t *)(pgd_now[pgd_idx] & PAGE_MASK);
  71. if (!pte) {
  72. return 0;
  73. }
  74. // 转换到内核线性地址
  75. pte = (pte_t *)((uint32_t)pte + PAGE_OFFSET);
  76. // 如果地址有效而且指针不为NULL,则返回地址
  77. if (pte[pte_idx] != 0 && pa) {
  78. *pa = pte[pte_idx] & PAGE_MASK;
  79. return 1;
  80. }
  81. return 0;
  82. }

需要注意的是Intel规定页表和页目录得的起始位置必须是页对齐的,attribute((aligned(PAGE_SIZE)))是GCC的扩展指令,功能是使得变量的起始地址按照某个数值对齐,所以我们轻轻松松的就解决了这个难题。

上面代码对应的头文件如下:

  1. #ifndef INCLUDE_VMM_H
  2. #define INCLUDE_VMM_H
  3. #include "types.h"
  4. #include "idt.h"
  5. #include "vmm.h"
  6. // 内核的偏移地址
  7. #define PAGE_OFFSET 0xC0000000
  8. /**
  9. * P-- 位 0 是存在 (Present) 标志,用于指明表项对地址转换是否有效。
  10. * P = 1 表示有效; P = 0 表示无效。
  11. * 在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。
  12. * 如果 P = 0 ,那么除表示表项无效外,其余位可供程序自由使用。
  13. * 例如,操作系统可以使用这些位来保存已存储在磁盘上的页面的序号。
  14. */
  15. #define PAGE_PRESENT 0x1
  16. /**
  17. * R/W -- 位 1 是读 / 写 (Read/Write) 标志。如果等于 1 ,表示页面可以被读、写或执行。
  18. * 如果为 0 ,表示页面只读或可执行。
  19. * 当处理器运行在超级用户特权级 (级别 0,1 或 2) 时,则 R/W 位不起作用。
  20. * 页目录项中的 R/W 位对其所映射的所有页面起作用。
  21. */
  22. #define PAGE_WRITE 0x2
  23. /**
  24. * U/S -- 位 2 是用户 / 超级用户 (User/Supervisor) 标志。
  25. * 如果为 1 ,那么运行在任何特权级上的程序都可以访问该页面。
  26. * 如果为 0 ,那么页面只能被运行在超级用户特权级 (0,1 或 2) 上的程序访问。
  27. * 页目录项中的 U/S 位对其所映射的所有页面起作用。
  28. */
  29. #define PAGE_USER 0x4
  30. // 虚拟分页大小
  31. #define PAGE_SIZE 4096
  32. // 页掩码,用于 4KB 对齐
  33. #define PAGE_MASK 0xFFFFF000
  34. // 获取一个地址的页目录项
  35. #define PGD_INDEX(x) (((x) >> 22) & 0x3FF)
  36. // 获取一个地址的页表项
  37. #define PTE_INDEX(x) (((x) >> 12) & 0x3FF)
  38. // 获取一个地址的页內偏移
  39. #define OFFSET_INDEX(x) ((x) & 0xFFF)
  40. // 页目录数据类型
  41. typedef uint32_t pgd_t;
  42. // 页表数据类型
  43. typedef uint32_t pte_t;
  44. // 页表成员数
  45. #define PGD_SIZE (PAGE_SIZE/sizeof(pte_t))
  46. // 页表成员数
  47. #define PTE_SIZE (PAGE_SIZE/sizeof(uint32_t))
  48. // 映射 512MB 内存所需要的页表数
  49. #define PTE_COUNT 128
  50. // 内核页目录区域
  51. extern pgd_t pgd_kern[PGD_SIZE];
  52. // 初始化虚拟内存管理
  53. void init_vmm();
  54. // 更换当前的页目录
  55. void switch_pgd(uint32_t pd);
  56. // 使用 flags 指出的页权限,把物理地址 pa 映射到虚拟地址 va
  57. void map(pgd_t *pgd_now, uint32_t va, uint32_t pa, uint32_t flags);
  58. // 取消虚拟地址 va 的物理映射
  59. void unmap(pgd_t *pgd_now, uint32_t va);
  60. // 如果虚拟地址 va 映射到物理地址则返回 1
  61. // 同时如果 pa 不是空指针则把物理地址写入 pa 参数
  62. uint32_t get_mapping(pgd_t *pgd_now, uint32_t va, uint32_t *pa);
  63. // 页错误中断的函数处理
  64. void page_fault(pt_regs *regs);
  65. #endif // INCLUDE_VMM_H

当CPU进入分页模式的时候,一旦发生内存访问的页错误,就会产生14号中断。上面注册的14号中断处理函数实现如下:

  1. #include "vmm.h"
  2. #include "debug.h"
  3. void page_fault(pt_regs *regs)
  4. {
  5. uint32_t cr2;
  6. asm volatile ("mov %%cr2, %0" : "=r" (cr2));
  7. printk("Page fault at 0x%x, virtual faulting address 0x%x\n", regs->eip, cr2);
  8. printk("Error code: %x\n", regs->err_code);
  9. // bit 0 为 0 指页面不存在内存里
  10. if ( !(regs->err_code & 0x1)) {
  11. printk_color(rc_black, rc_red, "Because the page wasn't present.\n");
  12. }
  13. // bit 1 为 0 表示读错误,为 1 为写错误
  14. if (regs->err_code & 0x2) {
  15. printk_color(rc_black, rc_red, "Write error.\n");
  16. } else {
  17. printk_color(rc_black, rc_red, "Read error.\n");
  18. }
  19. // bit 2 为 1 表示在用户模式打断的,为 0 是在内核模式打断的
  20. if (regs->err_code & 0x4) {
  21. printk_color(rc_black, rc_red, "In user mode.\n");
  22. } else {
  23. printk_color(rc_black, rc_red, "In kernel mode.\n");
  24. }
  25. // bit 3 为 1 表示错误是由保留位覆盖造成的
  26. if (regs->err_code & 0x8) {
  27. printk_color(rc_black, rc_red, "Reserved bits being overwritten.\n");
  28. }
  29. // bit 4 为 1 表示错误发生在取指令的时候
  30. if (regs->err_code & 0x10) {
  31. printk_color(rc_black, rc_red, "The fault occurred during an instruction fetch.\n");
  32. }
  33. while (1);
  34. }

整理好代码后进行编译,再用objdump查看可执行文件的段表,输出大致如下:

  1. hx_kernel file format elf32-i386
  2. section:
  3. Idx Name Size VMA LMA File off Algn
  4. 0 .init.text 00001000 00100000 00100000 00000094 2**0
  5. CONTENTS, ALLOC, LOAD, READONLY, CODE
  6. 1 .init.data 00001000 00101000 00101000 00001094 2**2
  7. CONTENTS, ALLOC, LOAD, DATA
  8. 2 .text 00003000 c0102000 00102000 00003000 2**4
  9. CONTENTS, ALLOC, LOAD, READONLY, CODE
  10. 3 .data 00001000 c0105000 00105000 00006000 2**2
  11. CONTENTS, ALLOC, LOAD, DATA
  12. 4 .bss 00105000 c0106000 00106000 00007000 2**12
  13. ALLOC
  14. 5 .stab 00005000 c020b000 0020b000 0010c000 2**2
  15. CONTENTS, ALLOC, LOAD, READONLY, DATA
  16. 6 .stabstr 00002000 c0210000 00210000 00111000 2**0
  17. CONTENTS, ALLOC, LOAD, READONLY, DATA

我们看到前两个区段和以前的输出类似,但是后面区段的VMA已经变成了加上了0xC0000000偏移的地址了。如果运行后能看到和上一章相同的输出结果就没有问题了。如果你得不到正确的结果,那就自己动手调试吧。

原文:

http://wiki.0xffffff.org/posts/hurlex-10.html