Linux 源码下载路径位于 https://mirrors.edge.kernel.org/pub/linux/kernel/,这篇博客所需要的 0.01 版本源码通过点击链接 https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/linux-0.01.tar.gz 可下载。
Linux 0.01 版本内核是一个 80386 平台版本的内核,所以需要了解 x86 平台的一些汇编知识以及 AT&T 汇编嵌入编程,不过没关系,我们可以边阅读代码边学习,遇到不懂的就暂停存档,然后就搜索学习,最后返回继续读档学习,这也是我学习 Linux 内核的基本思路。由于我的工作是在 ARM64 平台下,所以当之后的版本有了 ARM64 架构之后,将转移阵地到 ARM64 上面,影响应该不大。Linux 0.01 处理的物理内存大小是 8MB。
分段和分页的很多理论知识在网上可以找到很多,这里不再赘述这部分内容,我们聚焦的点是在 Linux 0.01 版本中如何实现分段和分页。在保护模式下程序使用的是逻辑地址,逻辑地址映射到物理地址要经过线性地址,这部分的映射是分段的内容,而分页是将线性地址映射到物理地址。在 x86 中,分段是必须的,分页是可选的,Linux 中分页功能是开启的。
如上图所示,在 x86 平台上,虚拟地址由两部分组成:16 位的段选择符和 32 位的偏移值,总共 48 位,我们称 32 位的偏移值位逻辑地址,在某些可辨清晰的情形下,可能也会将虚拟地址称为逻辑地址;线性地址和物理地址则都是 32 位。分段机制和分页机制都是通过查表实现的,这部分内容可以参考https://blog.csdn.net/leoufung/article/details/86770794 这篇文章,说的很详细。我们在这里将要说一下原理。
分段有四个术语:段选择符、偏移值、段描述符和段描述符表。段选择符和偏移值构成了逻辑地址,段描述符表是一个数组,段描述符是构成段描述符表的一个个 entry,每个段描述符占 8 个字节,作用是提供一个段的基地址、长度以及保护属性信息,这里我们主要关注的是段的基地址这个信息,这个基地址指的是一个段字节 0 在线性空间的位置。段描述符表有两种:全局描述符表(GDT)和局部描述符表(LDT)。在 Linux 中,每个任务都有一个 LDT。下面是逻辑地址转线性地址的参考图:
刚才说段描述符表有两种,但具体选择哪种,依靠段选择符中的一个比特位是 0 还是 1 来决定,这里不细说。总结一下就是逻辑地址划分为两部分:段选择符和偏移值,通过段选择符定位到在段描述符表中的索引,拿到段描述符表中的内容,提取出段基地址,然后加上偏移值得到的线性地址就是该逻辑地址在线性空间映射的地址了。
前提知识:每一页 4K。分页的做法和分段很类似,在 Linux 0.01 版本中,使用的是二级分页,有一个页目录和一个页表。具体做法如下:
这边不详细展开说了,如果上图看不懂,可评论或搜索其他资料。
我们探索的源码位于内核根目录下的 mm 目录,该目录下有两个文件 memory.c 和 page.s,其中 memory.c 文件时我们关注的重点,里面的程序实现了对物理内存的管理。
在 Linux 0.01 版本内核中能处理的最大物理内存是 8MB,include/linux/config.h
中定义了该内存大小:
/* #define LASU_HD */
#define LINUS_HD
/*
* Amount of ram memory (in bytes, 640k-1M not discounted). Currently 8Mb.
* Don't make this bigger without making sure that there are enough page
* directory entries (boot/head.s)
*/
#if defined(LINUS_HD)
#define HIGH_MEMORY (0x800000)
#elif defined(LASU_HD)
#define HIGH_MEMORY (0x400000)
#else
#error "must define hd"
#endif
/* End of buffer memory. Must be 0xA0000, or > 0x100000, 4096-byte aligned */
#if (HIGH_MEMORY>=0x600000)
#define BUFFER_END 0x200000
#else
#define BUFFER_END 0xA0000
#endif
这段代码中有两个宏 LASU_HD
和 LINUS_HD
,不清楚这两个宏是啥,知道的请不吝赐教。不过代码中默认定义了宏 LINUS_HD
,我们就按这个宏来走接下来的流程就好。因此 HIGH_MEMORY
是 8MB
,BUFFER_END
是 2MB
。BUFFER_END
是缓冲区末端。
在 memory.c
文件中定义了 LOW_MEM
如下:
#if (BUFFER_END < 0x100000)
#define LOW_MEM 0x100000
#else
#define LOW_MEM BUFFER_END
#endif
因此,LOW_MEM
位于 2MB
处。所以整个内存模型如下:
内存管理的区域是主内存区,分配和释放页面都是在主内存区。
setup_paging
要清楚的一点是内核中直接访问的地址是线性地址,在这里没有逻辑地址,我们可以说线性地址是一个指针变量,我们可以对其解引用得到里面的值,但是我们不能说物理地址是一个指针变量,它就是一个整数值,我们不能用它来直接解引用访问内存。这里我们要做的是建立页表,内核中为了很简单就可以感知到物理内存,内核做的映射是 identity-mapping,即线性地址映射的值与物理地址的值相等。这段代码位于 boot/head.s
文件中:
.org 0x1000
pg0:
.org 0x2000
pg1:
setup_paging:
movl $1024*3,%ecx ---------------------------- (1)
xorl %eax,%eax ---------------------------- (2)
xorl %edi,%edi /* pg_dir is at 0x000 */ ---------------------------- (3)
cld;rep;stosl ---------------------------- (4)
movl $pg0+7,_pg_dir /* set present bit/user r/w */ ---------------------------- (5)
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg1+4092,%edi ---------------------------- (6)
movl $0x7ff007,%eax /* 8Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */ ---------------------------- (7)
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax ---------------------------- (8)
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
eax
寄存器的值为 0,目的是后面的 stosl
指令会用到 eax
寄存器。edi
寄存器的值为 0,目的是设置 stlos
指令从物理地址 0 的地方开始填充 eax
,为什么是从 0 开始,因为 Linux 0.01 版本内核的页目录 pg_dir
设在 0 位置。cld
指令清除方向标志位 DF
,即 DF=0
.这个指令用于串操作指令中。先了解下串操作指令,在串操作指令中,源操作数和目的操作数分别使用寄存器 (e)si
和 (e)di
进行间接寻址,每执行一次串操作,源指针 (e)si
和目的指针 (e)di
将自动进行修改:±1、±2、±4,其对应的分别是字节操作、字操作和双字操作。这里的 ±1、±2、±4 是加还是减就是取决于 DF
方向标志位,当 DF=1
时为减,当 DF=0
时为加(与 cld
指令相对的 std
指令)。rep
指令是重复执行后面的指令 ecx
次,也就是执行 stosl
指令 ecx
次,stosl
指令将 eax
中的值保存到 es:edi
指向的地址中,这里要说明 es:edi
,es
表示的是将要访问哪一个数据段,在保护模式下只有一个数据段,所以 es
的值默认和 ds
一样,如果是在实模式中,就要自己设置 es
的值了,由于 eax
的值为0,所以这段代码目的是将这三个页表内容清零。cr3
控制寄存器)cr0
控制寄存器)mem_map
Linux 0.01 版本内核通过一个数组来管理主内存区,这个数组名为 mem_map
。
/* these are not to be changed - thay are calculated from the above */
#define PAGING_MEMORY (HIGH_MEMORY - LOW_MEM)
#define PAGING_PAGES (PAGING_MEMORY/4096)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
static unsigned short mem_map [ PAGING_PAGES ] = {0,};
这段代码的思想是将主内存区分为 4K 为一段来管理,mem_map
的一个 entry 项代表一个 4K 的页,当其值为 0 时,表示该页没有被占用,大于 0 表示被占用,大于 1 表示页面被共享。MAP_NR
求给定地址 addr
所在页面在 mem_map
数组的索引。
get_free_page
通过 get_free_page
函数在主内存区申请一页空闲内存,申请成功返回物理内存的页基址,若无空闲内存页则返回 0。
/*
* Get physical address of first (actually last :-) free page, and mark it
* used. If no free pages left, return 0.
*/
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax"); ------------------------------- (1)
__asm__("std ; repne ; scasw\n\t" ------------------------------- (2)
"jne 1f\n\t" ------------------------------- (3)
"movw $1,2(%%edi)\n\t" ------------------------------- (4)
"sall $12,%%ecx\n\t" ------------------------------- (5)
"movl %%ecx,%%edx\n\t" ------------------------------- (6)
"addl %2,%%edx\n\t" ------------------------------- (7)
"movl $1024,%%ecx\n\t" ------------------------------- (8)
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n" ------------------------------- (9)
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
:"di","cx","dx");
return __res;
}
__res
变量,register
关键字指示编译器将 __res
变量放在寄存器中,后面的 asm("ax")
指定寄存器为 ax
寄存器,这里使用寄存器变量是因为在嵌入汇编语言中想要将汇编指令的输出直接写到指定的寄存器中的话,使用寄存器变量会很方便。寄存器变量的一些知识参考 《Linux 内核完全注释》一书如下:__asm__
表示这是嵌入汇编,关于这部分知识可以自己在网上搜索学习下格式。\n\t
是为了编译器预处理出来的汇编程序对齐好看,可以方便查看调试汇编程序。重点是 std ; repne ; scasw
,这里有三个指令,std
指令使方向标志位 DF
置位,即 DF=1
.这个指令用于串操作指令中。先了解下串操作指令,在串操作指令中,源操作数和目的操作数分别使用寄存器 (e)si
和 (e)di
进行间接寻址,每执行一次串操作,源指针 (e)si
和目的指针 (e)di
将自动进行修改:±1、±2、±4,其对应的分别是字节操作、字操作和双字操作。这里的 ±1、±2、±4 是加还是减就是取决于 DF
方向标志位,当 DF=1
时为减,当 DF=0
时为加(与 std
指令相对的 cld
指令)。REPNE
指令一般用来扫描字符串,它用来重复后一个指令,这里是 scasw
指令,REPNE
的重复条件是 ZF=0 且 ecx > 0
,每循环执行一次,ecx
的值自动减 1,所以如果当它后面的指令扫描字符串时,当它扫到字符串与需要检测的字符串相等时或者扫描了 ecx
次之后停止扫描,后一个条件好理解,因为扫描了 ecx
次之后,ecx
的值已经为 0,已经不满足重复条件了;而扫描到相等则会置 ZF=1
,也会使条件不满足。最后一个指令 scasw
,最后那个 w
表示字,两个字节,这个指令减 ax
寄存器和 di
寄存器的值对比,每比较一次,di
寄存器依赖 DF
的值自动增加或减小,增加或减小的值如果指令是 scasw
则为 2,如果是 scasb
则为 1。ax
寄存器的初始赋值在第 21 行,"0" (0)
中 "0"
(或者”“)表示使用输出操作数中同一位置的寄存器,即寄存器 ax
,(0)
表示赋值为 0;di
寄存器的初始赋值在第 22 行,"D" (mem_map+PAGING_PAGES-1)
中的 "D"
表示引用寄存器 edi
,后面的 (mem_map+PAGING_PAGES-1)
表示赋值为 mem_map
数组的最后一个索引,所以查找空闲页面是从最后一页开始查找的。jne 1f
表示 ZF=0
则跳转到 1
标签,ZF=0
表示没有找到空闲页,跳转到 1
标签,结束嵌入汇编,然后直接返回 __res
,也就是 0。1 => [2 + edi]
,edi
寄存器加 2 是因为执行完 scasw
指令之后 edi
会减 2,加 2 回到找到的空闲页的地址,这里找到空闲页,将对应的 mem_map
的值置为 1。ecx
左移 12 位,ecx
的值会随着 repne
指令变化,找到空闲页之后,ecx
为空闲页的 mem_map
数组索引,左移 12 位即是空闲页的相对于 LOW_MEM
的页面基址。ecx
移到 edx
。edx
加上 LOW_MEM
则为空闲页面的实际物理地址,由于内核是线性一一映射,所以 edx
也是线性地址。ecx
的值设为 1024,作用是为后面的 rep 指令充当计数器,循环 1024 次,leal
指令将 edi
指向空闲页的最后倒数第 4 个字节,stosl
指令将 eax
中的值保存到 es:edi
指向的地址中,这里要说明 es:edi
,es
表示的是将要访问哪一个数据段,在保护模式下只有一个数据段,所以 es
的值默认和 ds
一样,如果是在实模式中,就要自己设置 es
的值了,由于 eax
的值为0,所以这段代码目的是将物理页面内容清零。eax
的值为空闲页面的物理地址,也就是设置了 __res
的值,最后返回 __res
。free_page
通过 free_page
释放一个页面。
/*
* Free a page of memory at physical address 'addr'. Used by
* 'free_page_tables()'
*/
void free_page(unsigned long addr)
{
if (addr<LOW_MEM) return; ------------------------------------- (1)
if (addr>HIGH_MEMORY) ------------------------------------- (2)
panic("trying to free nonexistent page");
addr -= LOW_MEM; ------------------------------------- (3)
addr >>= 12;
if (mem_map[addr]--) return; ------------------------------------- (4)
mem_map[addr]=0; ------------------------------------- (5)
panic("trying to free free page");
}
LOW_MEM
,表示要释放的地址不位于主内存区,对此不做处理。HIGH_MEMORY
(等于 HIGH_MEMORY
也是不合法的,在 0.11 版本会更正),表示要释放的地址不在于我们所认知的物理地址范围之内,所以 panic,Linux 0.01 版本的 panic
函数实现很简单:kernel/panic.c
volatile void panic(const char * s)
{
printk("Kernel panic: %s\n\r",s);
for(;;);
}
这里可以说下在这里 volatile
的作用,它修饰的是函数返回值,作用与修饰变量不同,它的作用是帮助编译器做优化,表示该函数不会返回了,所以如果在调用时也将返回地址压栈的话是没有意义的,volatile
可以提示编译器做这类优化,相当于 void panic(const char * s) __attribute__ ((noreturn));
3. 求出 addr
在 mem_map
数组中的索引,也就是 MAP_NR
宏实现的功能,不清楚这里为啥不直接使用这个宏。
4. 将 mem_map
中对应的索引的值减 1,即该页面数的引用减少 1。
5. 如果跑到这一行则表示原本 mem_map[addr] == 0
,但由于上一行 if
语句中将其减了 1, 所以这里将其直接还原成 0,然后 panic。
free_page_tables
free_page_tables
函数释放整个页表,它有两个参数:
from
,from
至少 4M 对齐,因为这个函数要释放的是整个页表,一个页表最大能表示 4M 的地址,所以要释放从 from
起的连续 4M 线性地址空间,那么 from
就要 4M 对齐。size
:这个 size
要 round_up
到 4M 对齐,譬如 0 的话就是 0,1.1 M 就是 4M,4.1M 是 8M。。。exit()
函数会用到这个函数,先照着解读吧:/*
* This function frees a continuos block of page tables, as needed
* by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
if (from & 0x3fffff) --------------- (1)
panic("free_page_tables called with wrong alignment");
if (!from) --------------- (2)
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22; --------------- (3)
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ --------------- (4)
for ( ; size-->0 ; dir++) {
if (!(1 & *dir)) --------------- (5)
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir); --------------- (6)
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table) --------------- (7)
free_page(0xfffff000 & *pg_table);
*pg_table = 0;
pg_table++;
}
free_page(0xfffff000 & *dir); ---------------- (8)
*dir = 0;
}
invalidate(); ----------------- (9)
return 0;
}
from
要 4M 对齐。from
是 0 的话,那就是相当于要释放掉内核段所在的 4M 线性空间,那当然是不行的了。size
向上取整到 4M 对齐,并且除以 4M,譬如 size
传参为 5M,那么向上取整就是 8M,就是 2 个 4M,所以 size
的值就是 2.dir
最终得到的值是 from
对应的页目录的索引值,在 分页 一节中图中有看到线性地址的高 10 位指示页目录项,所以将 from
右移 22 位就得到了在页目录的第几项,又因每项占 4 个字节,所以还得乘以 4,也就是还要左移 2 位,才能得到 from
在页目录表中的对应索引的地址,总的来说,就是先右移 22 位,再左移 2 位,这个过程相当于 dir = (unsigned long *) ((from>>20) & 0xffc);
。可能是省时吧。。。*dir
取得页目录项里面的内容,也就是页表基址和页表属性,页表属性低 12 位,所以要与上 0xfffff000
。free_page
释放该页。cr3
控制寄存器。#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))
因为页目录的基地址就是 0,所以将 0 加载进 cr3
就可以了。
copy_page_tables
这个函数主要是供 fork()
函数使用,给定映射的源线性地址 from
、目的线性地址 to
和线性空间大小 size
,看懂了 free_page_tables
的代码,看这段代码也基本没啥问题,所以这段代码挑之前没涉及的点说。
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024; -------------- (1)
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;
*to_page_table = this_page;
if (this_page > LOW_MEM) { --------------- (2)
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();shua
return 0;
}
from
是 0 的话,表示要复制的是内核段,这段最多只要 640K 内存肯定完全覆盖内核了,所以只要复制 640K / 4K = 160 = 0xA0
项,而其他的要复制 1024 项。fork
的时候子进程和父进程数据段共享一个物理页,它们的页面属性设置为只读,只有当写的时候,重新分配一页,然后设置属性为可读写。低于 LOW_MEM
是内核使用的,一直都是共享的,不用修改这个属性。mem_map
数组是管理 LOW_MEM
以上内存的。put_page
put_page
函数将物理页面 page
映射到线性空间 address
。
unsigned long put_page(unsigned long page,unsigned long address)
{
unsigned long tmp, *page_table;
/* NOTE !!! This uses the fact that _pg_dir=0 */
if (page < LOW_MEM || page > HIGH_MEMORY) -------------- (1)
printk("Trying to put page %p at %p\n",page,address);
if (mem_map[(page-LOW_MEM)>>12] != 1) --------------- (2)
printk("mem_map disagrees with %p at %p\n",page,address);
page_table = (unsigned long *) ((address>>20) & 0xffc);
if ((*page_table)&1)
page_table = (unsigned long *) (0xfffff000 & *page_table);
else {
if (!(tmp=get_free_page()))
return 0;
*page_table = tmp|7;
page_table = (unsigned long *) tmp;
}
page_table[(address>>12) & 0x3ff] = page | 7; --------------- (3)
return page;
}
put_page
操纵的页面是从 get_free_page
中获取的,也就是来自于主内存区的,按理说,这里检查不通过应该直接返回的,因为运行下去 mem_map
数组有可能溢出,这里仅仅是打印出错信息,可能是因为内核中不会以这种不正确的页面来调用吧。。。(address>>12) & 0x3ff
是找到 address
在页表中的索引。un_wp_page
和 do_wp_page
un_wp_page
函数取消写保护,但是这个函数在 Linux 0.01 版本中实现有点问题,它在修改了页表项之后没有刷新 TLB,不过在之后的版本加了刷新。而且这个函数的参数是页表项的地址,这点与前面的函数都不一样。如果读懂了前面的函数,这个函数也挺好理解的,就不解析了。
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry;
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) { /* 页面不共享 */
*table_entry |= 2; /* 这里之后应该 invalidate(); */
return;
}
if (!(new_page=get_free_page())) /* 有共享页面, mem_map[MAP_NR(old_page)] > 1 */
do_exit(SIGSEGV);
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--;
*table_entry = new_page | 7; /* 这里之后也应该 invalidate(); */
copy_page(old_page,new_page);
}
do_wp_page
函数的参数中的地址和 un_wp_page
中的地址参数不一样,do_wp_page
中是页面中的一个地址,并不是页表项地址。还有一个参数是错误码 error_code
,这个错误码是进程在写写保护页面时由 CPU 自动产生的。要看懂这个函数,只要知道如何通过页面线性地址求得页表项线性地址就可以了。
/*
* This routine handles present pages, when users try to write
* to a shared page. It is done by copying the page to a new address
* and decrementing the shared-page counter for the old page.
*/
void do_wp_page(unsigned long error_code,unsigned long address)
{
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 &
*((unsigned long *) ((address>>20) &0xffc)))));
}
write_verify
这个函数的功能主要时验证页面可不可写,不可写就复制一个页面,取消写保护,这就是 un_wp_page
的功能,所以函数实现判断不可写,然后调用 un_wp_page
就好。
void write_verify(unsigned long address)
{
unsigned long page;
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) /* 页面不存在,就没有共享和写保护可言,直接返回就好 */
return;
page &= 0xfffff000;
page += ((address>>10) & 0xffc);
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
un_wp_page((unsigned long *) page);
return;
}
do_no_page
有两个参数,error_code
是由进程在访问页面时 CPU 因缺页产生,address
是产生异常地页面线性地址。
void do_no_page(unsigned long error_code,unsigned long address)
{
unsigned long tmp;
if (tmp=get_free_page())
if (put_page(tmp,address))
return;
do_exit(SIGSEGV);
}
这里面有个 do_exit
函数,这个函数我归类在进程调度那块,所以现在不做介绍。
页异常有两种情况:
do_no_page
来处理。do_wp_page
来处理。cr2
寄存器中。页异常中断处理程序在 mm/page.s
文件中由汇编代码实现,如下:/*
* page.s contains the low-level page-exception code.
* the real work is done in mm.c
*/
.globl _page_fault
_page_fault:
xchgl %eax,(%esp) ---------------- (1)
pushl %ecx ---------------- (2)
pushl %edx
push %ds
push %es
push %fs
movl $0x10,%edx ----------------- (3)
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
movl %cr2,%edx ----------------- (4)
pushl %edx ----------------- (5)
pushl %eax
testl $1,%eax ----------------- (6)
jne 1f
call _do_no_page
jmp 2f
1: call _do_wp_page
2: addl $8,%esp ----------------- (7)
pop %fs ----------------- (8)
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
xchgl
指令交换 eax
寄存器的值和 esp
指向地址的内容,相当于在栈中保存了 eax
的值,并且将 esp
指向地址的内容交换到了 eax
,也就是错误码 error_code
。memory.c
中的缺页处理函数。boot/head.s
中的初始话中,内核代码段和内核数据段都设置成了长度 8MB 的段,应该使为了访问页目录表,内核数据段位于 gdt 表的第三个索引,每个索引项占 8 个字节,所以第三个就是 0x10。_gdt: .quad 0x0000000000000000 /* NULL descriptor,没有用的 */
.quad 0x00c09a00000007ff /* 8Mb,内核代码段 */
.quad 0x00c09200000007ff /* 8Mb,内核数据段 */
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
do_wp_page
函数,否则转到调用 do_no_page
。汇编代码中加了个下划线 ‘-’,这是因为 gcc
在编译 C 代码时会在函数名下加下划线,所以汇编代码也要这么做才能找到对应的函数。pop
指令,直接用一条 add
指令,提高效率。iret
进行中断返回,使程序返回到原来发生中断的地方。资料站点:http://oldlinux.org/