从开机加电到main函数之前过程

16位实模式 -> 32位保护模式

EP/EIP 相当于pc指针,从实模式16位->保护模式32位

BIOS

加电后进入实模式运行,16位。

上电后CS(代码段寄存器)设置为0XF000(纯硬件完成),因此第一条程序跳到0XF000执行。0XF000是BIOS的程序入口地址,因此此时主动权交到了BIOS手上

image-20230926154112305

如图可以看出BIOS的中断向量表有0x400大小->1024->1KB

BIOS 的中断向量表256个中断向量:cs+ip

bios 执行int0x19中断,将磁盘的第一个扇区(bootsect.s的程序)复制到0x07C00处

image-20230927080114009

bootsect.s

此时位于实模式,寻址空间1MB (\(2^{20}\))

image-20230927090739464

第一步:将bootsect的程序(一个扇区512字节)复制到0x90000处,并跳转过去执行(0x90000-0x901FF)

CS寄存器存储当前代码段的起始地址,IP寄存器存储下一条要执行的指令的偏移量,它们共同组成了8086处理器中的程序计数器(PC),指导CPU按照指令序列执行程序。

一开始cs在0x7c0

DS:SI(偏移量) 和 ES:DI(偏移量) 是在x86汇编语言中常用于字符串操作的寄存器组合。它们通常在字符串复制、字符串比较和其他字符串处理操作中一起使用。在字符串操作中,DS:SI 组合通常用于指示源字符串(例如,要复制的字符串),而 ES:DI 组合通常用于指示目标字符串(例如,复制到的目标位置)。这些寄存器组合使汇编程序员能够高效地复制、比较或处理字符串数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
start:
mov ax,#BOOTSEG
mov ds,ax !
mov ax,#INITSEG
mov es,ax
mov cx,#256 !一个字是2字节-》512字节就是第一扇区的大小,循环256次
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax

因此上一段程序中

设置了 ds:si BOOTSEG:0x0000 es:di INITSEG :0x9000

CX 寄存器是x86架构中的一个16位通用寄存器,它常常用于循环计数和计数相关的操作。CX 寄存器的前身是8086处理器中的 CX 寄存器,后来的x86处理器也保留了这个寄存器,并且它在汇编语言和编程中仍然非常重要。

rep 是一个前缀指令,通常与一些数据传输或操作指令一起使用,如 movsstoscmps 等。它表示重复执行指定的操作,直到满足某个条件为止。

​ movw是指一次移动两个字节,因此这里就是重复操作移动2个字节,重复256次

image-20230927094109074

在x86汇编语言中,CS:IP 寄存器组合(也称为程序计数器,PC)用于指示当前正在执行的指令的内存地址。跳转指令(如jmpcallret 等)可以影响 CS:IP 寄存器组合,从而改变程序的控制流。更加深入了解请参考链接:https://cloud.tencent.com/developer/article/1680474

实模式下物理地址的计算方式是:CS * 16 + IP。 段基址寻址参考博客:https://mp.ofweek.com/it/a256714278117

执行完这段循环,执行go语句。go转移指令会导致pc指针的变化,即ip寄存器被修改为(0x90000)。因此下一条语句执行mov ax,cscs段变为INITSEG段

完成复制以后,cs ip段通过jmpi go,INITSEG跳转继续执行,此时0x07c00和0x90000的代码是一致的

1
2
3
go:	mov	ax,cs
mov ds,ax
mov es,ax

上述代码将其它寄存器的值做了调整,压栈方向从高地址到低地址

image-20230927134028219

第二步 加载磁盘2~5个扇区的setup程序

1
2
3
4
5
6
7
8
9
10
11
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup

int 0x19 实际上是int 0x13的一个特例。注意这俩都属于BIOS的中断函数

int 0x13,将扇区加载到指定地址的内存

image-20231003092439766

可以看出bootsect的起始位置是0x90000,占用512字节-》0x901FF

bootsect将setup代码对应的四个扇区加载到SETUPSEG:0x9020紧挨着bootsect

第三步:加载内核代码

仍使用int 0x13 将240个扇区加载到内存

1
2
3
4
mov	ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
1
2
3
4
5
6
7
8
9
10
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret

调用 read_it 将240个扇区加载到SYSSEG =0x100,首先将SYSSEG赋值给ax,ax又赋值给es,其中es是int0x13读取时的输入参数,

jne die:根据前面的测试结果,如果 ax 的低12位不全为零(即测试结果不等于零),则跳转到标签 die,进入无限循环

image-20231003100723241

跳到setup

1
2
3
4
5
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:

jmpi 0,SETUPSEG !0代表偏移量是0,目标地址是SETUPSEG
image-20231003101530040

setup.s

第一步,卸磨杀驴,建立设备信息表

image-20231003102018552

由于bootesect已经运行完了,所以把设备信息表建立再原来bootesect程序的部分。即0x90000~0x901FD

实现实模式向保护模式跨越

在此之前,系统运行在实模式。现在要执行以下操作:

  • 打开32位寻址空间

  • 打开保护模式

  • 建立保护模式下的中断响应机制

  • 建立内存分页机制

中断机制建立

首先关闭bios中断,废除bios中断机制

image-20231003105617140
1
2
3
4
5
! now we want to move to protected mode ...

cli ! no interrupts allowed !

! first we move the system to it's rightful place
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	mov	ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment es:di是目的地址(0x0000:0x0)
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment ds:si 源地址 0x1000:0x0
sub di,di
sub si,si
mov cx,#0x8000 !移动0x8000字,即32kB字=64KB字节
rep
movsw
jmp do_move

! then we load the segment descriptors

将位于0x10000的内核程序复制到0x0000处,覆盖原来的BIOS中断向量表和数据区

这一部分具有head.s和main里面的kernel代码

image-20231003145416320

下一步建立新的中断机制

通过GDT(全局描符表),GDTR(gdt基址寄存器),IDT 中断描述符表,IDTR(IDT 基址寄存器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax ! ds指向本程序
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
gdt:
.word 0,0,0,0 ! dummy
!第0项,空的
!第一项,这里在gdt的表中偏移量是0x08.当加载代码段寄存器CS的时候,使用的是这个偏移值,指向内核代码段
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
!第二项,这里在gdt的表中偏移量是0x10.当加载数据段寄存器 DS的时候,使用的是这个偏移值,指向内核数据段
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386

idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L

gdt_48:
.word 0x800 ! 0x800=2^(3+8)=2048=2kB字节,8B组成一个描述项因此256 GDT entries
.word 512+gdt,0x9 ! gdt base = 0X9xxxx 定义了gdt的基址
!0x0009<<16+0x0200+gdt :32位基地址

这里的gdt表定义了三项

  1. lidt idt_48: 这是加载中断描述符表(Interrupt Descriptor Table,IDT)的指令。它用 idt_48 指定的地址加载了 IDT。IDT 是用于存储中断和异常处理程序地址的数据结构。

  2. lgdt gdt_48: 这是加载全局描述符表(Global Descriptor Table,GDT)的指令。它用 gdt_48 指定的地址加载了 GDT。GDT 是用于描述段属性的数据结构,包括代码段和数据段的起始地址和限制。 代码解读:

.word 是汇编语言中的伪指令,通常用于定义数据或者常量。它告诉汇编器将后面的值以16位或32位的形式存储在内存中。.word 可以用于定义字、半字(16位)或者双字(32位)等数据。

在上面提到的汇编代码中,.word 用于定义 GDT 的限制字段和基址字段。例如,.word 0x800 定义了一个16位的字,其值为 0x800.word 512+gdt, 0x9 定义了两个16位的字,分别是 512+gdt0x9

.word 0x800: 这一行定义了 GDT 的限制字段(limit)。0x800 是一个16位的值,表示 GDT 的大小。在这里,它的值是 0x800,对应于2048字节。这表示 GDT 中可以包含2048/8=256个条目。

GDT GDT的数据结构是一个描述符数组,每个描述符8个字节,可以存放在内存当中任意位置:

image-20231003111510079

一个GDT段描述符占用8个字节,包含三个部分:

段基址(32位),占据描述符的第16~39位和第55位~63位,前者存储低16位,后者存储高16位 段界限(20位),占据描述符的第0~15位和第48~51位,前者存储低16位,后者存储高4位。 段属性(12位),占据描述符的第39~47位和第49~55位,段属性可以细分为8种:TYPE属性、S属性、DPL属性、P属性、AVL属性、L属性、D/B属性和G属性 image-20231003111819195

image-20231003160110786
image-20231003114017638

gdt表放在了是0x90200部分,也就是setup刚执行完的代码部分立刻就被覆盖用来干别的了。

其中gdt表的段基址和限长被保存在看GDTR中,保证gdt的唯一性。gdtr可以看成是一个指针

1
2
3
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L

可以看出IDT此时只是搭了一个框架,还没有填东西,填东西这部分在后面main函数的内核部分完成。同理idtr指向了idt

打开A20,实现32位寻址

IBM 公司最初推出的个人计算机 IBM PC 使用的 CPU 是 Intel 8088。在该微机中地址 线只有 20 根(A0 – A19)。其所能寻址的最高地址是 0xffff:0xffff,也即 0x10ffef。对于超出 0x100000(1MB)的寻址地址 将默认地==环绕==到 0x0ffef。当 IBM 公司于 1985 年引入 AT 机时,使用的是 Intel 80286 CPU,具有 24 根地 址线,最高可寻址 16MB,并且有一个与 8088 完全兼容的实模式运行方式。然而,在寻址值超过 1MB时它却不能象 8088 那样实现地址寻址的环绕。但是当时已经有一些程序是利用这种地址环绕机制进行工 作的。为了实现完全的兼容性,IBM 公司发明了使用一个开关来开启或禁止 0x100000 地址比特位。由 于在当时的 8042 键盘控制器上恰好有空闲的端口引脚(输出端口 P2,引脚 P21),于是便使用了该引脚 来作为与门控制这个地址比特位。该信号即被称为 A20。如果它为零,则比特 20 及以上地址都被清除。 从而实现了兼容性。

1
2
3
4
5
6
7
8
! that was painless, now we enable A20
call empty_8042 ! 等待输入缓冲器空
mov al,#0xD1 ! command write,0xD1 表示要写数据到
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042

在汇编语言中,out 是一个用于向特定端口发送数据的指令。它通常用于与外部设备或I/O端口进行通信。

out #0x64, al: 这一行使用 out 汇编指令将 al 中的值 0xD1 写入端口 0x64。在这里,端口 0x64 是用于与键盘控制器通信的端口。通过向该端口发送命令或数据,可以控制键盘的行为或向键盘发送数据。

地址由原来的1M扩展为4G

image-20231003145504688

打开保护模式

1
2
3
mov	ax,#0x0001	! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)

CR0 控制寄存器(32位)用于存放系统控制标志,

  • CR0的第0位是PE(protection enable 开启保护模式的标志位) 置零为实模式,置一为保护模式

  • CR0的第31位是paging即(分页的标志位)

    lmsw 是汇编语言中的指令,用于加载机器状态字寄存器(Machine Status Word Register,MSW)指令执行后,源操作数中的值将被加载到机器状态字寄存器中,从而改变了处理器的一些状态和特性。这个指令通常用于操作系统内核或特权级别的代码,以更改处理器的运行模式或特权级别

划重点 jmpi 0, 8是什么意思

跳转到内核代码段,从setup跳转到head.s开始执行

分析:0指的是段内偏移为0,8指的是段选择符。8的二进制:8(0b0000 0000 0000 1000)这里最后两个0代表特权级00内核特权级,11代表用户特权级,第三位0代表全局描述符表即GDT如果是1代表局部描述符表LDT。第四位的1代表是GDT中的第一项

1
2
3
4
5
6
7
8
9
10
11
12
13
gdt:
.word 0,0,0,0 ! dummy
!第0项,空的
!第一项,这里在gdt的表中偏移量是0x08.当加载代码段寄存器CS的时候,使用的是这个偏移值,指向内核代码段
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)段限长8MB
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
!第二项,这里在gdt的表中偏移量是0x10.当加载数据段寄存器 DS的时候,使用的是这个偏移值,指向内核数据段
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386

第一项是cs段,可以用于确定段基址和段限长等信息。这里段基址目前为0x0000,偏移前面说了是0,因此代码跳转到0x0000处执行。而这一部分是之前0x10000部分的代码,在废除BIOS的中断之后,这部分代码被搬移到了0x0000处也就是head.s+main里面的kernel。所以这里跳转到head.s开始执行。(这部分代码首先由bootsect加载到0x10000,然后在setup.s打开保护模式的过程中挪到了0x0000处)

image-20231003160040393

head.s

完善保护模式

1
2
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:

_pg_dir,用于标识内核分页后内核的起始位置,也就是物理内存的起始位置0x000000

设置DS,esp栈等

1
2
3
4
5
6
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs

可知在jmpi 0,8部分CS已经从实模式的寻址转变到了保护模式寻址,这一段代码是将其它寄存器也转变到保护模式

ds,es,fs,gs设置为0x10 。 0x10:b(10000)特权级是0,全局描述符表,第10也就是第三项,即内核数据段。

image-20231003162657094
1
lss _stack_start,%esp

_stack_start 这种加了下滑线的代表在C语言里面也有定义:

kernel/sched.c

1
2
3
4
5
struct {
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
//#define PAGE_SIZE 4096 因此这里是user_stack [1024]

这段代码定义了一个结构体(struct)stack_start,该结构体包含两个成员:

  1. a:一个指向长整数(long)的指针。在这里,a 被初始化为指向 user_stack[PAGE_SIZE>>2] 的指针,其中 PAGE_SIZE>>2 表示将 PAGE_SIZE 右移两位,相当于将 PAGE_SIZE 除以 4,然后 user_stack 是一个数组,这个指针 a 最终指向数组 user_stack 中的某个元素。
  2. b:一个短整数(short)。在这里,b 被初始化为 0x10,表示十六进制数 0x10,也就是十进制数 16。

所以,这段代码的目的是创建一个名为 stack_start 的结构体,并初始化其中的两个成员,一个是指针 a 指向数组 user_stack 中的某个元素,另一个是短整数 b 被初始化为十六进制数 0x10(十进制数 16).

将32位栈顶指针指向user_stack数据结构的最末位置如下所示。

image-20231003163507170

在内核初始化操作过程中被用作内核栈,初始化完成以后将被用作task0的用户态堆栈。

设置IDT

中断描述符:

image-20231003165058386
1
2
3
4
5
6
7
8
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */

lea _idt,%edi /*_idt中断描述符表的地址*/
mov $256,%ecx
image-20231003165553164

lea 是 x86 汇编语言中的一条指令,它代表 "Load Effective Address",用于将一个有效地址加载到目标寄存器中。lea 指令并不执行内存引用操作,而只是将 ignore_int 变量的内存地址(有效地址)加载到 edx 寄存器中ignore_int 是中断门。

1
_idt:	.fill 256,8,0		# idt is uninitialized

这行汇编指令的作用是创建一个名为 _idt 的数组,这个数组的长度是 256 个元素,每个元素占据 8 个字节,并且将所有元素初始化为 0。这种语法通常用于声明并分配一块内存,用于存储中断描述符表(Interrupt Descriptor Table,IDT)的内容。 IDT 通常用于 x86 架构的操作系统中,用于处理中断和异常。

废除已有的GDT新建GDT

因为原有的gdt表建在setup那段程序中,后面会被用来干别的覆盖。

1
2
3
setup_gdt:
lgdt gdt_descr
ret
1
2
3
4
5
gdt_descr:
.word 256*8-1 # so does gdt (not that that's any
.long _gdt # magic number, but it works for me :^)

.align 3
  • .word 256*8-1 表示 GDT 的限制(limit)。在这里,256*8-1 是 GDT 表的限制值,表示 GDT 中可以存放的描述符数量。每个描述符占用 8 个字节,所以限制值设置为 256*8-1 表示 GDT 中可以存放 256 个描述符。
  • .long _gdt 表示 GDT 的基地址(base address)。在这里,_gdt 是 GDT 数组的地址,这个地址将作为 GDT 表的基地址。
  • .align 3 指令用于确保接下来的数据或代码在内存中按照 2^3 = 8 字节对齐。这通常用于对齐数据或代码,以提高访问效率。
1
2
3
4
5
_gdt:	.quad 0x0000000000000000	/* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */
.quad 0x00c0920000000fff /* 16Mb 为啥是16M:段限长:0x00fff,C:1100 G=1粒度是4k ,0~0fff->2^12*4k=16M*/
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc 有增加了一项空项*/

.quad 是汇编中的伪指令,用于定义 64 位(8 字节)的数据项

0x00c09a0000000fff /* 16Mb */为啥?

16Mb 为啥是16M:段限长:0x00fff,C:1100 G=1粒度是4k ,0~0fff->2^124k=16M

前八位是段基址,即C0。

注意后三位的FFF是段限长,说明段限长由原来的7FF(8MB)变为了16MB

image-20231003171101035

因为段限长变了,所以要对DS,ES,FS,GS,SS进行修改,每个描述符的限长也修改了,因此需要重新加载一次

1
2
3
4
5
6
movl $0x10,%eax		# reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp
image-20231003192816784
image-20231003201544973

测试A20是否打开

1
2
3
4
5
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b

测试A20是否真的打开了image-20231003193634497

main函数压栈

1
2
3
4
5
6
7
8
9
10
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.

将L6和main函数入口地址压栈,当head.s执行完以后执行·ret就会pop栈顶指针,跳转到main函数执行,这里面的栈就是上面的栈

image-20231003194434372

建立分页机制

完成main函数的压栈后,开始建立分页机制

首先从内存起始位置放一个页目录表和四个页表,每页4KB

1
2
3
4
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
image-20231003195656562

设置页目录表前四项使之分别指向四个页表。

32位地址:CPU 在看到我们给出的内存地址后,首先把线性地址被拆分成

高 10 位:中间 10 位:后 12 位。

线性地址的位 31-22 共 10 个比特用来确定页目录中的目录项,位 21-12 用来寻址页目录项指定的页 表中的页表项,最后的 12 个比特正好用作页表项指定的一页物理内存中的偏移地址。

image-20231006092145578

\(2^{10}\)=1K,一个页目录表有1k个页表,一个页表包含1k个页表项,一个页表指向一个页面(4k)。

因此一个页表对应了4M的物理空间。这里一共建立了4个页表:16M空间,理论上可以更大。由于硬件中cpu是4k对齐的

因此低12位会是零,所以可以用高20位来表示4KB对齐的页表和页,因此可以用后12位设置权限。

image-20231006092929709

其中,页框地址(PAGE FRAME ADDRESS)指定了一页内存的物理起始地址。因为内存页是位于 4K边界上的,所以其低 12 比特总是 0,因此表项的低 12 比特可作它用。图中的存在位(PRESENT – P)确定了一个页表项是否可以用于地址转换过程。P=1 表示该项可用。 当目录表项或第二级表项的 P=0 时,则该表项是无效的,不能用于地址转换过程。此时该表项的所有其 它比特位都可供程序使用;处理器不对这些位进行测试。

当 CPU 试图使用一个页表项进行地址转换时,如果此时任意一级页表项的 P=0,则处理器就会发出 页异常信号。此时缺页中断异常处理程序就可以把所请求的页加入到物理内存中,并且导致异常的指令 会被重新执行。

已访问(Accessed – A)和已修改(Dirty – D)比特位用于提供有关页使用的信息。除了页目录项中 的已修改位,这些比特位将由硬件置位,但不复位。

在对一页内存进行读或写操作之前,CPU 将设置相关的目录和二级页表项的已访问位。在向一个二 级页表项所涵盖的地址进行写操作之前,处理器将设置该二级页表项的已修改位,而页目录项中的已修 改位是不用的。当所需求的内存超出实际物理内存量时,内存管理程序就可以使用这些位来确定那些页 可以从内存中取走,以腾出空间。内存管理程序还需负责检测和复位这些比特位。

读/写位(Read/Write – R/W)和用户/超级用户位(User/Supervisor – U/S)并不用于地址转换,但用 于分页级的保护机制,是由 CPU 在地址转换过程中同时操作的。

image-20231003225759273
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.org 0x1000
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000


1
2
3
4
5
6
7
8
9
   movl $pg0+7,_pg_dir		/* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
/*在这段汇编代码中,_pg_dir 是一个地址,_pg_dir+4 是将 _pg_dir 的地址值增加 4 个字节,
以访问 _pg_dir 中的下一个 4 字节的位置。这种情况通常出现在处理数组或数据结构的情况下,
需要按字节访问连续的内存位置。
在汇编语言中,符号$通常用于表示立即数(immediate value),而不是地址偏移。$pg2+7
中的7是一个立即数,而不是偏移量。这个7代表了一个具体的数值,而不是表示字节或二进制值。 */

上面的7代表111,这三位分别代表用户u,读写rw,存在p。如果是000代表内核,只读,不存在页。加7就是把后12位用于存权限问题。movl $pg0+7,_pg_dir 这行汇编指令的含义是将地址 pg0 加上 7 存储到 _pg_dir 地址处。因此上面这一段代码的意义就是让页目录表指向每个页表项,同时第一个地方指向页目录自己,每次递增4个字节即32位地址。也就是如下图所示,这里的_pg_dir就是head的起始地址,这里是在一边执行head.s的代码一边覆盖。

image-20231003232851762
1
2
3
4
5
6
7

movl $pg3+4092,%edi/*一个页表的最后一项在页表中的位置是1023*4=4092(一共1024项,第1024项的开始地址) */
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) 一个页面4k=4096,16M是整个页面管理的空间,所以这里是最后一个页面的地址*/
std/*方向位置位,edi递减 */
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax /*每写好一项物理地址递减0x1000 ,16^3=2^12=4k */
jge 1b/*如果小于0说明全填好了 */

edi指向了第一个页面,eax指向了最后一个页面

设置CR3,CR0

1
2
3
4
5
6
xorl %eax,%eax		/* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* 依赖前面的push stack转到main函数运行。this also flushes prefetch-queue */

CR3是页目录基址寄存器,其高20位存放页目录表基地址,这里就是0。orl $0x80000000,%eax,将最高位置一。==CR0寄存器==的第31位是分页机制控制位,置一代表开启分页。至此内核分页构建完成

image-20231003235254879
image-20231006092016126

内核内存恒等映射

根据内核分页为线性地址恒等映射的要求,推导出四个页表的映射公式,写出页表的设置代码。

在Linux 0.11 内核中,为了有效地使用机器中的物理内存,内存被划分成几个功能区域

image-20231005095121645

在 Linux 0.11 内核中,给每个程序(进程)都划分了总容量为 64MB 的虚拟内存空 间。因此程序的逻辑地址范围是 0x0000000 到 0x4000000。

在内存分段系统中,一个程序的逻辑地址是通过分段机制自动地映射(变换)到中间层的线性地址 上。每次对内存的引用都是对内存段中内存的引用。当一个程序引用一个内存地址时,通过把相应的段 基址加到程序员看得见的逻辑地址上就形成了一个对应的线性地址。此时若没有启用分页机制,则该线 性地址就被送到 CPU 的外部地址总线上,用于直接寻址对应的物理内存。

转到main

ret返回,结合前面所说的main函数压栈,这里返回,得到main函数的入口地址,接下来就执行main函数了。

image-20231003235415139

小结

image-20231003150359552

实模式和保护模式的寻址区别

在实模式中,CPU通过段地址和段偏移量寻址。其中段地址保存到段寄存器,包含:CS、SS、DS、ES、FS、GS。段偏移量可以保存到IP、BX、SI、DI寄存器。在汇编代码mov ds:[si], ax中,会将AX寄存器的数据写入到物理内存地址DS * 16 + SI中。

而在保护模式下,也是通过段寄存器和段偏移量寻址,但是此时段寄存器保存的数据意义不同了。 此时的CS和SS寄存器后13位相当于GDT表中某个描述符的索引,即==段选择子==。第2位存储了TI值(0代表GDT,1代表LDT),第0、1位存储了当前的特权级(CPL)。

image-20231003111510079
image-20231003150641596
image-20231003201644907

分页后寻址

image-20231003201757163

高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。

intel CPU 使用段(Segment)的概念来对程序进行寻址。每个段定义了内存中的某个区域以及访问 的优先级等信息。而每个程序都可有若干个内存段组成。访问控制是基于段的访问控制

程序的==逻辑地址==(或称为虚拟地址)即是用于 寻址这些段和段中具体地址位置。在 Linux 0.11 中,程序逻辑地址到线性地址的变换过程使用了 CPU 的 全局段描述符表 GDT 和局部段描述符表 LDT。由 GDT 映射的地址空间称为全局地址空间,由 LDT 映 射的地址空间则称为局部地址空间,而这两者构成了虚拟地址的空间。

image-20231005100625737