操作系统真象还原<第三部分>

(。・∀・)ノ゙嗨起来!!!

向内核迈进

获取物理内存容量

inux获取内存容量方法有三种,本质上分别是BIOS中断0x15的3个子功能,BIOS是实模式下的方法,只能在保护模式之前调用。

利用BIOS中断0x15子功能0xe820获取内存

此方法最灵活,返回的内容也最丰富,内存信息的内容是地址范围描述符来描述的(ARDS),每个字段4字节,一共20字节,调用0x15返回的也就是这个结构。其中Type字段表示内存类型,1表示这段内存可以使用;2表示不可用使用此内存;其它表示未定义,将来会用到。

image-20201022190231494

用0x15子功能0xe820调用说明和调用步骤如下

  1. 填写好”调用前输入”中列出的寄存器

  2. 执行中断调用 int 0x15

  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

    image-20201022190424025

利用BIOS中断0x15子功能0xe801获取内存

此方法最多识别4G的内存,结果存放在两组寄存器中,操作起来要简便一些,调用说明和调用步骤如下

  1. AX寄存器写入0xE801
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20201022190558027

利用BIOS中断0x15子功能0x88获取内存

此方法最多识别64MB内存,操作起来最简单,调用说明和调用步骤如下

  1. AX寄存器写入0x88
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20201022190849105

下面结合这三种方式改进我们的实验代码,下面是loader,我们将结果保存在了total_mem_bytes中。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
   %include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000

CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4

DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl为0

GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的空位(slot)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start:

;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。


;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------------- 加载GDT ----------------
lgdt [gdt_ptr]

;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt: ;出错则挂起
hlt

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax

mov byte [gs:160], 'P'

jmp $

mbr.S中也需要修改一处内容,我们跳转的内容要加上0x300,原因是在 loader.S 中loader_start计算如下

(4个段描述符 + 60个段描述符槽位) * 8字节 = total_mem_bytes_offset

(4 + 60) * 8 = 512 = 0x200

total_mem_bytes + gdt_ptr + ards_buf + adrs_nr + total_mem_bytes_offset = loader_start

4 + 6 + 244 + 2 + 0x200 = 0x300

修改片断如下

1
2
3
4
5
6
7
8
9
10
[...] 
mov eax,LOADER_START_SECTOR
mov bx,LOADER_BASE_ADDR
mov cx,4
call rd_disk_m_16

jmp LOADER_BASE_ADDR+0x300 ; 这里

rd_disk_m_16:
[...]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
添加个库目录 + 编译
--nasm -I include/ -o mbr.bin mbr.S
下面将生成的 mbr.bin 写入我们的虚拟硬盘,还是用 dd 命令。
--sudo dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc
--sudo dd if=/home/fyz/sc/bochs-2.6.2/mbr.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc

记录了1+0 的读入
记录了1+0 的写出
512 bytes copied, 0.000546068 s, 938 kB/s

添加个库目录 + 编译
--nasm -I include/ -o loader.bin loader.S
将生成的 loader.bin 写入硬盘第 2 个扇区。第 0 个扇区是 MBR,第 1 个扇区是空的未使用。( 我就喜欢 你咋的
--sudo dd if=./loader.bin of=/your_path/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc
--sudo dd if=./loader.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=2 seek=2 conv=notrunc
(由于是1000字节大小 所以 count=2

记录了1+1 的读入
记录了1+1 的写出
1000 bytes (1.0 kB) copied, 0.000761711 s, 1.3 MB/s

运行结果如下,这里我们用xp 0xb00查看我们的结果,0x02000000换算过来刚好是我们bochsrc.disk中 megs 设置的32MB大小

image-20201022204208877

启动分页机制

分页机制是当物理内存不足时,或者内存碎片过多无法容纳新进程等情况的一种应对措施。假如说此时未开启分页功能,而物理内存空间又不足,如下图所示.

image-20201023164539146

此时进程C想要执行,但是内存空间已经不足。要么就等待进程A或者进程B执行完成,这样就有连续的内存空间了。要么就讲进程A的A3段或者进程B的B1段换到硬盘上,腾出一部分空间,同样可以容纳进程C执行.

等待是极其不好的用户体验,那么只能将段置换到硬盘上了,但是段的大小并不固定,如何段过大,那么IO操作过多,机器的响应速度就会非常慢。

出现这种情况的本质其实是在分段机制下,线性地址等价于物理地址。那么即使在进程B的下面还有10M的可用空间,但因为两块可用空间并不连续,所以进程C无法使用进程B下面的10M可用空间。

按照这种思路,只需要通过某种映射关系,将线性地址映射到任意的物理地址,就可以解决这种问题了。实现线性地址的连续,而物理地址不需要连续,于是分页机制就诞生了。

一级页表

尽管在保护模式中段寄存器中的内容已经是选择子,但选择子最终就是为了要找到段基址,其内存访问的核心机制依然是“段基址:段内偏移地址”,这两个地址在相加之后才是绝对地址,也就是我们所说的线性地址,此线性地址在分段机制下被 CPU 认为是物理地址,直接拿来就能用,也就是说,此线性地址可以直接送上地址总线。

分页机制是工作在分段机制下的,在保护模式下,通过选择子找到段基址,通过段基址:段内偏移的方式组合成线性地址,拿到线性地址之后会根据是否开启分页来找到实际的物理地址。

image-20201023165737231

分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。

分页机制的作用有两方面。

1.将线性地址转换成物理地址。
2.用大小相等的页代替大小不等的段

如图所示:

需要通过分页机制来映射的线性地址便是我们通常叫的 虚拟地址

image-20201023170120868

我们需要平衡页的大小与页的数量的关系,因为页大小*页数量=4GB,想要减少页表的大小,只能增加一页的大小。最终通过数学求极限,定下4KB为最佳页大小。

image-20201023171618309

这种情况下,4GB的内存被划分为1MB个内存块,每个内存块的大小为4KB。

页表和内存的映射关系如图

image-20201023171819242

页部件的工作:用线性地址的高 20 位在页表中索引页表项,用线性地址的低 12 位与页表项中的物理地址相加,所求的和便是最终线性地址对应的物理地址。

例如 mov ax,[0x1234] 。

image-20201023172031920

二级页表

无论是几级页表,标准页的尺寸都是4KB。所以4GB的线性地址空间最多有1M个标准页。一级页表是将这1M个标准页放置到一张页表中,二级页表是将这1M个标准页平均放置1K个页表中,每个页表包含有1K个页表项。页表项是4字节大小,页表包含1K个页表项,故页表的大小同样为4KB,刚好为一页。

为了管理页表的物理地址,专门有一个页目录表来存放这些页表。页目录表中存储的页表称为页目录项(PDE),页目录项同样为4KB,且最多有1K个页目录项,所以页目录表也是4KB,如下图所示

image-20201023201356840

二级页表中虚拟地址到物理地址的转换也有很大的变化,具体步骤如下

  • 用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。

  • 用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。

  • 虚拟地址的高 10 位和中间 10 位分别是 PDE PIE 的索引值,所以它们需要乘以 4。但低 12 位就不是索引值了,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第二步中得到的物理页地址,所得的和便是最终转换的物理地址。

    下图表示mov ax, [0x1234567]的转换过程,可以发现cr3寄存器其实指向的是页目录表基地址

image-20201023201922880

PDE和PTE的结构如下图所示

image-20201023202117714

从右到左各属性总结如下表

属性位 意义
P 存在位,为1时表示该页在物理内存中,为0表示不在物理内存中
RW 读写位,为1时可读可写,为0是可读不可写
US 特权位,为1时表示处于普通用户,03特权级可访问,为0表示超级用户,02特权级可访问
PWT 页级通写位,为1表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存
PCD 页级高速缓存禁止位,为1表示该页启用高速缓存
A 访问位,为1表示该页被CPU访问过
D 脏页位,当CPU对一个页面执行写操作,此为被赋1
PAT 页属性表位,能够在页面一级的粒度上设置内存属性
G 全局位,为1表示该页在高速缓存TLB中一直保存
AVL 表示软件,系统可用该位,和CPU无关

启用分页机制,我们要按顺序做好三件事。

(1)准备好页目录表及页表。
(2)将页表地址写入控制寄存器 cr3。
(3)寄存器 cr0 的 PG 位置 1。

image-20201023204710038

CR0 是系统内的控制寄存器之一。控制寄存器是一些特殊的寄存器,它们可以控制CPU的一些重要特性。
第 0 位保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
第 1 位监控协处理位MP(Moniter coprocessor),它与第3位一起决定:当TS=1时操作码WAIT是否产生一个“协处理器不能使用”的出错信号。
第 2 位模拟协处理器位EM (Emulate coprocessor),如果EM=1,则不能使用协处理器,如果EM=0,则允许使用协处理器。
第 3 位任务转换位TS(Task Switch),当一个任务转换完成之后,自动将它置1。随着TS=1,就不能使用协处理器。
第 4 位微处理器的扩展类型位 ET(Processor Extension Type),其内保存着处理器扩展类型的信息,如果ET=0,则标识系统使
用的是287协处理器,如果 ET=1,则表示系统使用的是387浮点协处理器。
第16位写保护未即WP位(486系列之后),只要将这一位置0就可以禁用写保护,置1则可将其恢复
第31位分页允许位PG(Paging Enable),它表示芯片上的分页部件是否允许工作。

CR1是未定义的控制寄存器,供将来的处理器使用。
CR2是页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。
CR3是页目录基址寄存器,保存页目录表的物理地址,页目录表总是放在以4K字节为单位的存储器边界上,因此,它的地址的低12位总为0,不起作用,即使写上内容,也不会被理会。
CR4在Pentium系列(包括486的后期版本)处理器中才实现,它处理的事务包括诸如何时启用虚拟8086模式等

启动分页机制

image-20201109200141630

创建页目录及页表的代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
; 创建页目录及页表
setup_page:
; 先把页目录占用的空间逐字节清零
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir

; 开始创建页目录项(PDE)
.create_pde: ; 创建PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 此时eax为第一个页表的位置及属性
mov ebx, eax ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址

; 下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存
; 这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表
; 这是为将地址映射为内核地址做准备
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一个页表项占用四字节
; 0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间
; 也就是页表的0xc0000000~0xffffffff这1G属于内核
; 0x0~0xbfffffff这3G属于用户进程
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址

; 下面创建页表项(PTE)
mov ecx, 256 ; 1M低端内存 / 每页大小 4K = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7
.create_pte: ; 创建PTE
mov [ebx+esi*4], edx ; 此时的edx为0x101000,也就是第一个页表的地址
add edx, 4096
inc esi
loop .create_pte

; 创建内核其他页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 属性为7
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

在boot.inc中添加如下信息

1
2
3
4
5
6
7
8
; loader 和 kernel
PAGE_DIR_TABLE_POS equ 0x100000
; 页表相关属性
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b

行完第一步的内容,之后的操作相对就简单了,将页表地址写入控制寄存器cr3寄存器和将cr0的PG位置1的操作整合起来的loader.S如下所示

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000

CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4

DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4

VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl为0

GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的空位(slot)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start:

;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。


;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------------- 加载GDT ----------------
lgdt [gdt_ptr]

;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt: ;出错则挂起
hlt

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax

; 创建页目录及页表并初始化内存位图
call setup_page

; 要将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
sgdt [gdt_ptr] ; 储存到原来gdt所有位置

; 将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2] ; gdt地址
or dword [ebx + 0x18 + 4], 0xc0000000
; 视频段是第3个段描述符,每个描述符是8字节,故0x18
; 段描述符的高4字节的最高位是段基址的第31~24位

; 将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000 ; 将栈指针同样映射到内核地址

; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

; 在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载

mov byte [gs:160], 'V'
; 视频段段基址已经被更新,用字符V表示virtual addr
jmp $

;------------- 创建页目录及页表 ---------------
; 创建页目录以及页表
setup_page:
; 页目录表占据4KB空间,清零之
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir

; 创建页目录表(PDE)
.create_pde:
mov eax, PAGE_DIR_TABLE_POS
; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址
add eax, 0x1000
mov ebx, eax

; 设置页目录项属性
or eax, PG_US_U | PG_RW_W | PG_P
; 设置第一个页目录项
mov [PAGE_DIR_TABLE_POS], eax
; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
; 最后一个表项指向自己,用于访问页目录本身
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax

; 创建页表
mov ecx, 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov [ebx + esi * 4], edx
add edx, 4096
inc esi
loop .create_pte

; 创建内核的其它PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

运行之后 ( 已成功

image-20201109210146952

运行结果如下图,其中 gdt段基址 已经修改为大于 0xc0000000,也就是3GB之上的内核地址空间,通过info tab可查看地址映射关系,其中箭头左边是虚拟地址,右边是对应的物理地址

image-20201109210300503

总结虚拟地址获取物理地址的过程:

先要从 CR3 寄存器中获取页目录表物理地址,然后用虚拟地址的高 10 位乘以 4 的积作为在页目录表中的偏移量去寻址目录项 pde ,从 pde 中读出页表物理地址,然后再用虚拟地址的中间 10 位乘以 4 的积作为在该页表中的偏移量去寻址页表项 pte,从该 pte 中读出页框物理地址,用虚拟地址的低 12 位作为该物理页框的偏移量。

总结用虚拟地址获取页表中各数据类型的方法:

获取页目录表物理地址:让虚拟地址的高 20 位为 0xfffff,低 12 位为 0x000,即 0xfffff000,这也是页目录表中第 0 个页目录项自身的物理地址。
访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为 0xfffffxxx,其中 xxx 是页目录 项的索引乘以 4 的积。
访问页表中的页表项:要使虚拟地址高 10 位为 0x3ff,目的是获取页目录表物理地址。中间 10 位 为页表的索引,因为是 10 位的索引值,所以这里不用乘以 4。低 12 位为页表内的偏移地址,用来定位页 表项,它必须是已经乘以 4 后的值。 公式为 0x3ff<<22+中间 10 位<<12+低 12 位。

快表 TLB

TLB,即 Translation Lookaside Buffer,俗称快表 :处理器准备了一个高速缓存,可以匹配高速的处理器速率和低速的内存访问速度,它专门用来存放虚拟地址页框与物理地址页框的映射关系。( 调整缓存

image-20201110174049306

TLB 中的条目是虚拟地址的高 20 位到物理地址高 20 位的映射结果,实际上就是从虚拟页框到物理页框的映射。除此之外 TLB 中还有一些属性位,比如页表项的 RW 属性。 有了 TLB,处理器在寻址之前会用虚拟地址的高 20 位作为索引来查找 TLB 中的相关条目,如果命中 (匹配到相关条目)则返回虚拟地址所映射的物理页框地址,否则会查询内存中的页表,获得页框物理地址后再更新 TLB。

更新TLB的方法有两种,重新加载CR3指令invlpg m,其中m表示操作数为虚拟内存地址,如更新虚拟地址0x1234对应的条目指令为invlpg [0x1234];

ELF格式浅析

elf格式的二进制文件

ELF 指的是 Executable and Linkable Format,可执行链接格式。最初是由 UNIX 系统实验室(USL) 作为应用程序二进制接口(ABI)而开发和发行的。工具接口标准委员会(TIS)选择了它作为 IA32 体系 结构上不同操作系统之间的可移植二进制文件格式,于是它就发展成为了事实上的二进制文件格式标准。

elf目标文件有待重定位文件,可共享目标文件,可执行文件。这里重点描述可执行文件。

image-20201112212147244

节、段

节(section)是指在汇编源码中由section或segment修饰,的指令区域或者数据区域。不同的汇编器关键字可能不一样。汇编器将这两个关键字修饰的区域在目标文件中编译成节,所以说节是在待重定位文件目标文件时产生的。
段(segment)是链接器根据待重定位文件中属性相同的多个section合并成section集合,这个集合就是段(segment)。链接器最后生成的是可执行文件,所以段是在可执行文件中生成的。平时所说的数据段,代码段就是这里说的segment。

节头表、程序头表、节头、程序头

一个程序中,节和段的大小和数量是不固定的,所以就需要一个专门的表来描述它们,这个表就是所说的节头表(section header table)和程序头表(program header table)。节头表中描述的信息是多个节头(section header),段头表中描述的信息是多个段头(program header)。在表中,每一个成员称为条目,及entry,一个条目代表一个段或者一个节的头信息。

elf header

因为程序中的段和节的大小和数量不确定,所以程序头表和节头表的大小就是不确定的,表在程序文件中的存储顺序也是由先后的,所以这两个表的位置也是不确定的。这时就必须在用一个固定的结构来描述这些不确定的信息,这个结构就是elf header,它位于文件最开始的部分。

elf文件格式作用是在文件链接和运行时方便进行程序的重定位。

elf 结构

ELF 格式的作用体现在两方面,一是链接阶段, 另一方面是运行阶段,它们在文件中组织布局从这两方面展示。

image-20201112212754030

有关elf 的任何定义,包括变量、常量及取值范围,都可以在 Linux 系统的 /usr/include/elf.h 中找到,这里面的定义才是最全最权威的。

elf header

用到的数据类型如下

数据类型名称 字节大小 对齐 意义
Elf32_Half 2 2 无符号中等大小的整数
Elf32_Word 4 4 无符号大整数
Elf32_Addr 4 4 无符号程序运行地址
Elf32_Off 4 4 无符号的文件偏移量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//elf header结构
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ //elf文件的魔数
Elf32_Half e_type; //elf文件的格式
Elf32_Half e_machine; //描述elf文件的体系结构类型
Elf32_Word e_version; //版本信息
Elf32_Addr e_entry; //操作系统运行程序时,将控制权交到的虚拟地址
Elf32_Off e_phoff; //程序头表在文件内的偏移量
Elf32_Off e_shoff; //节头表在文件内的偏移量
Elf32_Word e_flags; //与处理器相关的标志
Elf32_Half e_ehsize; //elf header的大小
Elf32_Half e_phentsize; //程序头表中每个条目的大小
Elf32_Half e_phnum; //程序头表中条目的数量
Elf32_Half e_shentsize; //节头表中每个条目的大小
Elf32_Half e_shnum; //节头表中条目的数量
Elf32_Half e_shstrndx; //string name table在节头表中的索引
} Elf32_Ehdr;
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
# xxd hello的结果,只截取了elf header相关的部分
00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 0300 0100 0000 e003 0000 3400 0000 ............4...
00000020: a817 0000 0000 0000 3400 2000 0900 2800 ........4. ...(.
00000030: 1d00 1c00 0600 0000 3400 0000 3400 0000 ........4...4...

#readelf -h hello的结果
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x3e0
程序头起点: 52 (bytes into file)
Start of section headers: 6056 (bytes into file)
标志: 0x0
本头的大小: 52 (字节)
程序头大小: 32 (字节)
Number of program headers: 9
节头大小: 40 (字节)
节头数量: 29
字符串表索引节头: 28

e_ident [16]是 16 字节大小的数组,用来表示 elf 字符等信息,开头的 4 个字 节是固定不变的,是 elf 文件的魔数,它们分别是 0x7f,以及字符串 ELF 的 ascall 码:0x45, 0x4c, 0x46。

image-20201112214041441

e_ident[5]大小端字节序, 可以利用 flie 指令来看。。。

e_type 占用2字节,指定 elf 目标文件的类型

elf目标文件类型 取值 意义
ET_NONE 0 未知目标文件格式
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 动态共享目标文件
ET_CORE 4 core文件,即程序崩溃时其内存映像的转储格式
ET_LOPROC 0xff00 特定处理器文件的扩展下边界
ET_HIPROC 0xffff 特定处理器文件的扩展上边界

取值为 2 的 ET_EXEC 类型,它的意义为程序可执行.

其余的字段

字段 大小(字节) 意义
e_machine 2 支持的硬件平台
e_version 4 表示版本信息
e_entry 4 操作系统运行该程序时,将控制权转交到的虚拟地址
e_phoff 4 程序头表在文件内的字节偏移量。如果没有程序头表,该值为0
e_shoff 4 节头表在文件内的字节偏移量。若没有节头表,该值为0
e_flags 4 与处理器相关的标志
e_ehsize 2 指明 elf header 的字节大小
e_phentsize 2 指明程序头表(program header table )中每个条目(entry)的字节大小
e_phnum 2 指明程序头表中条目的数量。实际上就是段的个数
e_shentsize 2 节头表中每个条目的字节大小,即每个用来描述节信息的数据结构的字节大小
e_shnum 2 指明节头表中条目的数量。实际上就是节的个数
e_shstrndx 2 指明 string name table 在节头表中的索引 index

e_machine 占用 2 字节,用来描述 elf 目标文件的体系结构类型,也就是说该文件要在哪种硬件平台(哪 种机器)上才能运行。

image-20201126082647857

struct Elf32_Phd

此段是指程序中的某个数据或代码的区域段落,例如数据段或代码段,并不是内存中的段,到现在为止我们都在讨论位于磁盘上的程序文件呢。struct Elf32_Phdr 结构的功能类似 GDT 中段描述符的作用,段描述符用来描述物理内存中的一个内存段,而 struct Elf32_Phdr 是用来描述位于磁盘上的程序中的一个段,它被加载到内存后才属于 GDT 中段描述符所指向的内存段的子集。

1
2
3
4
5
6
7
8
9
10
11
struct Elf32_Phdr
{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
};

各个字段意思

字段 意义
p_type 段类型
p_offset 本段在文件的偏移量
p_vaddr 本段在内存中起始的虚拟地址
p_paddr 仅用于与物理地址相关的系统中
p_filesz 本段在文件中的大小
p_memsz 本段在内存中的大小
p_flags 本段相关的标志
p_align 本段在文件和内存中的对齐方式

p_type 占用 4 字节,用来指明程序中该段的类型。

image-20201126083519569

p_flag的取值

image-20201126083630966

p_align 占用 4 字节,用来指明本段在文件和内存中的对齐方式。如果值为 0 或 1,则表示不对齐。否则 p_align 应该是 2 的幂次数。

ELF32_Shdr

每个节区头部可以用下面的数据结构进行描述:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
ELF32_Word sh_name;
ELF32_Word sh_type;
ELF32_Word sh_flags;
ELF32_Addr sh_addr;
ELF32_Off sh_offset;
ELF32_Word sh_size;
ELF32_Word sh_link;
ELF32_Word sh_info;
ELF32_Word sh_addralign;
ELF32_Word sh_entsize;
} Elf32_Shdr;
成员 说明
sh_name 节名称,是节区头字符串表节区中(Section Header String Table Section)的索引,因此该字段实际是一个数值。在字符串表中的具体内容是以 NULL 结尾的字符串。
sh_type 根据节的内容和语义进行分类,具体的类型下面会介绍。
sh_flags 每一比特代表不同的标志,描述节是否可写,可执行,需要分配内存等属性。
sh_addr 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0。
sh_offset 给出节区的第一个字节与文件开始处之间的偏移。SHT_NOBITS 类型的节区不占用文件的空间,因此其 sh_offset 成员给出的是概念性的偏移。
sh_size 此成员给出节区的字节大小。除非节区的类型是 SHT_NOBITS ,否则该节占用文件中的 sh_size 字节。类型为 SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间。
sh_link 此成员给出节区头部表索引链接,其具体的解释依赖于节区类型。
sh_info 此成员给出附加信息,其解释依赖于节区类型。
sh_addralign 某些节区的地址需要对齐。例如,如果一个节区有一个 doubleword 类型的变量,那么系统必须保证整个节区按双字对齐。也就是说,sh_addr%sh_addralignsh_addr%sh_addralign=0。目前它仅允许为 0,以及 2 的正整数幂数。 0 和 1 表示没有对齐约束。
sh_entsize 某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。反之,此成员取值为 0。

索引为零(SHN_UNDEF)的节区头也存在,此索引标记的是未定义的节区引用。这一项的信息如下

正如之前所说,索引为零(SHN_UNDEF)的节区头也存在,此索引标记的是未定义的节区引用。这一项的信息如下

字段名称 取值 说明
sh_name 0 无名称
sh_type SHT_NULL 限制
sh_flags 0 无标志
sh_addr 0 无地址
sh_offset 0 无文件偏移
sh_size 0 无大小
sh_link SHN_UNDEF 无链接信息
sh_info 0 无辅助信息
sh_addralign 0 无对齐要求
sh_entsize 0 无表项

特殊下标

节头表中比较特殊的几个下标如下

名称 含义
SHN_UNDEF 0 标志未定义的,丢失的,不相关的或者其它没有意义的节引用。例如,与节号SHN_UNDEF相关的“定义”的符号就是一个未定义符号。注:虽然0号索引被保留用于未定义值,节头表仍然包含索引0的项。也就是说,如果ELF头的e_shnum为6,那么索引应该为0~5。更加详细的内容在后面会说明。
SHN_LORESERVE 0xff00 保留索引值范围的下界。
SHN_LOPROC 0xff00 处理器相关的下界
SHN_HIPROC 0xff1f 处理器相关的上界
SHN_ABS 0xfff1 相关引用的绝对值。例如与节号SHN_ABS相关的符号拥有绝对值,它们不受重定位的影响
SHN_COMMON 0xfff2 这一节区相定义的符号是通用符号,例如FORTRAN COMMON,C语言中未分配的外部变量。
SHN_HIRESERVE 0xffff 保留索引值范围的上界。

系统保留在SHN_LORESERVESHN_HIRESERVE之间(包含边界)的索引值,这些值不在节头表中引用。也就是说,节头表不包含保留索引项。没特别理解。

部分节头字段

sh_type

节类型目前有下列可选范围,其中 SHT 是Section Header Table 的简写。

名称 取值 说明
SHT_NULL 0 该类型节区是非活动的,这种类型的节头中的其它成员取值无意义。
SHT_PROGBITS 1 该类型节区包含程序定义的信息,它的格式和含义都由程序来决定。
SHT_SYMTAB 2 该类型节区包含一个符号表(SYMbol TABle)。目前目标文件对每种类型的节区都只 能包含一个,不过这个限制将来可能发生变化。 一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。
SHT_STRTAB 3 该类型节区包含字符串表( STRing TABle )。
SHT_RELA 4 该类型节区包含显式指定位数的重定位项( RELocation entry with Addends ),例如,32 位目标文件中的 Elf32_Rela 类型。此外,目标文件可能拥有多个重定位节区。
SHT_HASH 5 该类型节区包含符号哈希表( HASH table )。
SHT_DYNAMIC 6 该类型节区包含动态链接的信息( DYNAMIC linking )。
SHT_NOTE 7 该类型节区包含以某种方式标记文件的信息(NOTE)。
SHT_NOBITS 8 该类型节区不占用文件的空间,其它方面和SHT_PROGBITS相似。尽管该类型节区不包含任何字节,其对应的节头成员sh_offset 中还是会包含概念性的文件偏移。
SHT_REL 9 该类型节区包含重定位表项(RELocation entry without Addends),不过并没有指定位数。例如,32位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。
SHT_SHLIB 10 该类型此节区被保留,不过其语义尚未被定义。
SHT_DYNSYM 11 作为一个完整的符号表,它可能包含很多对动态链接而言不必 要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
SHT_LOPROC 0X70000000 此值指定保留给处理器专用语义的下界( LOw PROCessor-specific semantics )。
SHT_HIPROC OX7FFFFFFF 此值指定保留给处理器专用语义的上界( HIgh PROCessor-specific semantics )。
SHT_LOUSER 0X80000000 此值指定保留给应用程序的索引下界。
SHT_HIUSER 0X8FFFFFFF 此值指定保留给应用程序的索引上界。
sh_flags

节头中 sh_flags 字段的每一个比特位都可以给出其相应的标记信息,其定义了对应的节区的内容是否可以被修改、被执行等信息。如果一个标志位被设置,则该位取值为1,未定义的位都为0。目前已定义值如下,其他值保留。

名称 说明
SHF_WRITE 0x1 这种节包含了进程运行过程中可以被写的数据。
SHF_ALLOC 0x2 这种节在进程运行时占用内存。对于不占用目标文件的内存镜像空间的某些控制节,该属性处于关闭状态(off)。
SHF_EXECINSTR 0x4 这种节包含可执行的机器指令(EXECutable INSTRuction)。
SHF_MASKPROC 0xf0000000 所有在这个掩码中的比特位用于特定处理器语义。

当节区类型的不同的时候,sh_link 和 sh_info 也会具有不同的含义。

sh_type sh_link sh_info
SHT_DYNAMIC 节区中使用的字符串表的节头索引 0
SHT_HASH 此哈希表所使用的符号表的节头索引 0
SHT_REL/SHT_RELA 与符号表相关的的节头索引 重定位应用到的节的节头索引
SHT_SYMTAB/SHT_DYNSYM 操作系统特定信息,Linux 中的 ELF 文件中该项指向符号表中符号所对应的字符串节区在 Section Header Table 中的偏移。 操作系统特定信息
other SHN_UNDEF 0

载入内核

Linux下可以用readelf命令解析ELF文件,下面是我们在kernel目录下新添加的测试代码,因为是64位操作系统,编译命令需要如下修改,我们下一步就是将这个简单的elf文件加载入内核,物理内存中0x900是loader.bin的加载地址,其开始部分是不能覆盖的GDT,预计其大小是小于2000字节,保守起见这里选起始的物理地址为0x1500,所以链接命令指定虚拟起始地址0xc0001500。

mian.c

1
2
3
4
int main(void){
while(1);
return 0;
}
1
2
3
--sudo gcc -m32 -c -o main.o main.c
--sudo ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o
--ll kernel.bin

下面通过dd命令将其写入磁盘,为了不纠结count的赋值,这里直接赋值为200,seek赋值为9,写在第9扇区

1
--sudo dd if=./kernel.bin of=/home/fyz/cx/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc

写完之后我们需要修改loader.S中的内容,分两步完成

  • 加载内核:内核文件加载到内存缓冲区
  • 初始化内核:需要在分页后,将加载进来的elf内核文件安置到相应的虚拟内存地址,然后跳过去执行,从此loader的工作结束

内核的加载地址选取的是0x7e00~0x9fbff范围中的0x70000,添加如下片断

1
2
3
4
5
6
7
8
9
10
; ------------------ 加载内核 ------------------
mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇区号0x9
mov ebx, KERNEL_BIN_BASE_ADDR ; 0x70000
; 从磁盘读出后,写入到ebx指定的地址
mov ecx, 200 ; 读入的扇区数

call rd_disk_m_32 ; eax,ebx,ecx均为参数,从硬盘上读取数据

; 创建页目录及页表并初始化页内存位图
call setup_page

下一步是初始化内核的工作,我们需要遍历kernel.bin程序中所有的段,因为它们才是程序运行的实质指令和数据的所在地,然后将各段拷贝到自己被编译的虚拟地址中,如下添加的是在loader.S中的内容,注释已经很详细了

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
   ; -------------------------   加载kernel  ----------------------
[略...]
; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

; 在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载

jmp SELECTOR_CODE:enter_kernel ; 强制刷新流水线,更新gdt,不刷新也可以
enter_kernel:
call kernel_init
mov esp, 0xc009f000 ;进入内核之后栈也要修改
jmp KERNEL_ENTRY_POINT ; 用地址0x1500访问测试,结果ok
;----------将kernel.bin中的segment拷贝到编译的地址----------
kernel_init:
xor eax, eax
xor ebx, ebx ; 记录程序头表地址
xor ecx, ecx ; cx记录程序头表中的program header数量
xor edx, edx ; dx记录program header尺寸,即e_phentsize

mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件开始部分28字节的地方是e_phoff,表示第1个program header在文件中的偏移量

add ebx, KERNEL_BIN_BASE_ADDR
mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header
.each_segment:
cmp byte [ebx + 0], PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。
je .PTNULL

;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)
push dword [ebx + 16] ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
mov eax, [ebx + 4] ; 距程序头偏移量为4字节的位置是p_offset
add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
push eax
push dword [ebx + 8] ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址
call mem_cpy ; 调用mem_cpy完成段复制
add esp,12 ; 清理栈中压入的三个参数, 3 * 4 = 12 字节
.PTNULL:
add ebx, edx ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
loop .each_segment
ret

;---------- 逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:
cld ; 控制重复字符递增方式,也就是edi和esi每复制一次就加一个单位大小,相对的指令为std
push ebp
mov esp, ebp
push ecx ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
mov edi, [ebp + 8] ; dst
mov esi, [ebp + 12] ; src
mov ecx, [ebp + 16] ; size
rep movsb ; 逐字节拷贝,直到ecx为0

; 恢复环境
pop ecx
pop ebp
ret

最终的一个内存布局如下,参考之前的1MB实模式地址图来对应就明白了

img

小总结:

修改一些文件所在目录 增加一个 boot 来放

1
2
3
4
5
6
7
8
.
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.bin
│ ├── loader.S
│ ├── mbr.bin
│ └── mbr.S

boot.inc

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
;---------------------------loader和kernel-------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
PAGE_DIR_TABLE_POS equ 0x100000
KERNEL_START_SECTOR equ 0x9
KERNEL_BIN_BASE_ADDR equ 0x70000
KERNEL_ENTRY_POINT equ 0xc0001500

; gdt描述符属性
; 段描述符高23位,表示段界限的粒度为4KB
DESC_G_4K equ 100000000000000000000000b

; D/B为,1表示运行在32位模式下
DESC_D_32 equ 10000000000000000000000b
; 高21位,如果为1表示为64位代码段,目前我们都是在32位模式下操作,故为零
DESC_L equ 0000000000000000000000b
; 没有明确的用途,取值随意
DESC_AVL equ 000000000000000000000b
; 第二部分段界限值,由于采用了32位平坦模型,所以段界限为(4GB / 4KB) - 1 = 0xFFFFF,故为全1
DESC_LIMIT_CODE2 equ 11110000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
; 书中取全零,怀疑是错误的,因为保护模式的基地址是0xb8000,所以最后8位应该是b,而不是0
DESC_LIMIT_VIDEO2 equ 00000000000000000000000000001011b
DESC_P equ 1000000000000000b
DESC_DPL_0 equ 000000000000000b
DESC_DPL_1 equ 010000000000000b
DESC_DPL_2 equ 100000000000000b
DESC_DPL_3 equ 110000000000000b
DESC_S_CODE equ 1000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0000000000000b
DESC_TYPE_CODE equ 100000000000b
DESC_TYPE_DATA equ 001000000000b

; 代码段描述符的高32位表示,其中(0x00 << 24表示最高8位的段基址值,由于我们采用的是平坦模型,故基址为零),后面唯一可变的就是段界限值
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \
DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00

DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00

DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00

; 选择字属性
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

; 页表相关属性
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b

PT_NULL equ 0

mbr.S

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
%include "boot.inc" 
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

mov ax,0x600
mov bx,0x700
mov cx,0
mov dx,0x184f
int 0x10

mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ;
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

mov eax,LOADER_START_SECTOR
mov bx,LOADER_BASE_ADDR
mov cx,4
call rd_disk_m_16

jmp LOADER_BASE_ADDR + 0x300

rd_disk_m_16:
mov esi,eax
mov di,cx

mov dx,0x1f2
mov al,cl
out dx,al

mov eax,esi

mov dx,0x1f3
out dx,al

mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al

mov dx,0x1f7
mov al,0x20
out dx,al

.not_ready:
nop
in al,dx
and al,0x88
cmp al,0x08
jnz .not_ready

mov ax,di
mov dx,256
mul dx
mov cx,ax
mov dx,0x1f0

.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret

times 510-($-$$) db 0
db 0x55,0xaa

loader.S

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
%include "boot.inc"

section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

; 这里其实就是GDT的起始地址,第一个描述符为空
GDT_BASE: dd 0x00000000
dd 0x00000000

; 代码段描述符,一个dd为4字节,段描述符为8字节,上面为低4字节
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4

; 栈段描述符,和数据段共用
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4

; 显卡段,非平坦
VIDEO_DESC: dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 120 dd 0
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

; 内存大小,单位字节,此处的内存地址是0xb00
total_memory_bytes dd 0

gdt_ptr dw GDT_LIMIT
dd GDT_BASE

ards_buf times 244 db 0
ards_nr dw 0

loader_start:

xor ebx, ebx
mov edx, 0x534d4150
mov di, ards_buf

.e820_mem_get_loop:
mov eax, 0x0000e820
mov ecx, 20
int 0x15

jc .e820_mem_get_failed

add di, cx
inc word [ards_nr]
cmp ebx, 0
jnz .e820_mem_get_loop

mov cx, [ards_nr]
mov ebx, ards_buf
xor edx, edx

.find_max_mem_area:
mov eax, [ebx]
add eax, [ebx + 8]
add ebx, 20
cmp edx, eax
jge .next_ards
mov edx, eax

.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

.e820_mem_get_failed:
mov byte [gs:0], 'f'
mov byte [gs:2], 'a'
mov byte [gs:4], 'i'
mov byte [gs:6], 'l'
mov byte [gs:8], 'e'
mov byte [gs:10], 'd'
; 内存检测失败,不再继续向下执行
jmp $

.mem_get_ok:
mov [total_memory_bytes], edx

; 开始进入保护模式
; 打开A20地址线
in al, 0x92
or al, 00000010B
out 0x92, al

; 加载gdt
lgdt [gdt_ptr]

; cr0第0位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

; 刷新流水线
jmp dword SELECTOR_CODE:p_mode_start

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax

mov es, ax
mov ss, ax

mov esp, LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax

; 加载kernel
mov eax, KERNEL_START_SECTOR
mov ebx, KERNEL_BIN_BASE_ADDR
mov ecx, 200

call rd_disk_m_32

call setup_page

; 保存gdt表
sgdt [gdt_ptr]

; 重新设置gdt描述符, 使虚拟地址指向内核的第一个页表
mov ebx, [gdt_ptr + 2]
or dword [ebx + 0x18 + 4], 0xc0000000
add dword [gdt_ptr + 2], 0xc0000000

add esp, 0xc0000000

; 页目录基地址寄存器
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

; 打开分页
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

lgdt [gdt_ptr]

; 初始化kernel
jmp SELECTOR_CODE:enter_kernel

enter_kernel:
call kernel_init
mov esp, 0xc009f000
jmp KERNEL_ENTRY_POINT

jmp $

; 创建页目录以及页表
setup_page:
; 页目录表占据4KB空间,清零之
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir

; 创建页目录表(PDE)
.create_pde:
mov eax, PAGE_DIR_TABLE_POS
; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址
add eax, 0x1000
mov ebx, eax

; 设置页目录项属性
or eax, PG_US_U | PG_RW_W | PG_P
; 设置第一个页目录项
mov [PAGE_DIR_TABLE_POS], eax
; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
; 最后一个表项指向自己,用于访问页目录本身
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax

; 创建页表
mov ecx, 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov [ebx + esi * 4], edx
add edx, 4096
inc esi
loop .create_pte

; 创建内核的其它PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

; 保护模式的硬盘读取函数
rd_disk_m_32:

mov esi, eax
mov di, cx

mov dx, 0x1f2
mov al, cl
out dx, al

mov eax, esi
; 保存LBA地址
mov dx, 0x1f3
out dx, al

mov cl, 8
shr eax, cl
mov dx, 0x1f4
out dx, al

shr eax, cl
mov dx, 0x1f5
out dx, al

shr eax, cl
and al, 0x0f
or al, 0xe0
mov dx, 0x1f6
out dx, al

mov dx, 0x1f7
mov al, 0x20
out dx, al

.not_ready:
nop
in al, dx
and al, 0x88
cmp al, 0x08
jnz .not_ready

mov ax, di
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0

.go_on_read:
in ax, dx
mov [ds:ebx], ax
add ebx, 2
loop .go_on_read
ret

kernel_init:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx

mov dx, [KERNEL_BIN_BASE_ADDR + 42]
mov ebx, [KERNEL_BIN_BASE_ADDR + 28]

add ebx, KERNEL_BIN_BASE_ADDR
mov cx, [KERNEL_BIN_BASE_ADDR + 44]

.each_segment:
cmp byte [ebx], PT_NULL
je .PTNULL

; 准备mem_cpy参数
push dword [ebx + 16]
mov eax, [ebx + 4]
add eax, KERNEL_BIN_BASE_ADDR
push eax
push dword [ebx + 8]

call mem_cpy
add esp, 12

.PTNULL:
add ebx, edx
loop .each_segment
ret

mem_cpy:
cld
push ebp
mov ebp, esp
push ecx

mov edi, [ebp + 8]
mov esi, [ebp + 12]
mov ecx, [ebp + 16]
rep movsb

pop ecx
pop ebp
ret

实行的操作

1
2
3
4
5
6
7
8
9
10
11
编译
--sudo nasm -I include/ -o mbr.bin mbr.asm
--sudo nasm -I include/ -o loader.bin loader.asm

下面将生成的 mbr.bin 写入我们的虚拟硬盘,还是用 dd 命令。
--sudo dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc
--sudo dd if=/home/fyz/sc/bochs-2.6.2/boot/mbr.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc

将生成的 loader.bin 写入硬盘第 2 个扇区。第 0 个扇区是 MBR,第 1 个扇区是空的未使用。( 我就喜欢 你咋的
--sudo dd if=./loader.bin of=/your_path/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
--sudo dd if=./loader.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=4 seek=2 conv=notrunc

特权级深入浅出

特权级简介

特权级按照权力从大到小分为 0、1、2、3 级,没错,数字越小,权力越大,0 级特权能力最大,3 级特权能力最小

image-20201201111343825

操作系统位于最内环的 0 级特权,它要直接控制硬件,掌控各种核心数据,所以它的权利必须最大。系统程序分别位于 1 级特权和 2 级特权,运行在这两层的程序一般是虚拟机、驱 动程序等系统服务。在最外层的是 3 级特权,我们的用户程序就运行在此层,用户程序被设计为“有需求时找操作系统”,所以它不需要太大的能力,能完成一般工作即可,因此它的权利最弱。

TSS 简介

任务状态段(Task-State Segment(TSS)),保存任务状态信息的系统段为任务状态段。

image-20201201112920210

TSS 是一种数据结构,它用于存储任务的环境。TSS主要分为动态字段和静态字段

在任务切换过程中当任务挂起时,处理器会更新动态字段,动态字段有:
 (1)通用寄存器字段—任务切换之前,EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI寄存器状态。
 (2)段选择符字段—任务切换之前,ES,CS,SS,DS,FS,GS寄存器保存的段选择符。
 (3)EFLAGS寄存器字段—任务切换之前,EFAGS寄存器的状态。
 (4)EIP字段—任务切换之前,EIP寄存器的状态。
 (5)先前任务链接字段—包含先前任务的TSS的段选择符。该字段禁止任务通过使用IRET指令返回先前的任务。

当任务创建时会创建静态字段,静态字段可读,不可写:
 (1)LDT段选择符字段—包含任务LDT的段选择符。
 (2)CR3控制寄存器字段—包含任务使用的页目录的物理基地址。CR3也被称为page directory base register(PDBR)页目录基址寄存器。
 (3)特权级0,1,2栈指针字段—包含栈段(因为任务运行在不同特权级下,需要不同的栈段,故相应的SS0,SS1,SS2)的段选择符的逻辑地址和栈的偏移(不同特权级相应的ESP0,ESP1,ESP2)。在特定的任务中该字段是静态的,但是如果栈切换发生在单个任务中,SS和EIP的值就会改变。
 (4)T标志(调试陷阱,100字节,位0)—如果设置,当切换任务时,会引起调试异常。
 (5)I/O映射基址字段—是16位的偏移,包含从I/O权限位图和中断导向表中TSS的基地址。

TSS 是每个任务都有的结构,它用于一个任务的标识,相当于任务的身份证,程序拥有此结构才能运行,这是处理器硬件上用于任务管理的系统结构,处理器能够识别其中每一个字段。该结构看上去也有点复杂,里面众多寄存器都囊括到这 104 字节中啦,其实这 104 字节只是 TSS 的最小尺寸(其中有很多寄存器信息,而TSS则是由TR寄存器加载的),根据需要,还可以再接上个 IO 位图,这些内容将在后面章节用到时补充。这里目前只需要关注 28 字节之下的部分,这里包括了 3 个栈指针。

每个特权级只能有一个栈,特权级在变换的时候需要用到不同特权级下的栈,特权转移分为两类,一类是中断门和调用门实现低权限到高权限,另一类是由调用返回指令从高权限到低权限,这是唯一一种让处理器降低权限的方法。

对于低权限到高权限的情况,处理器需要提前记录目标栈的地方,更新SS和ESP,也就是说我们只需要提前在TSS中记录好高特权级的栈地址即可,也就是说TSS不需要记录3级特权的栈,因为它的权限最低。

对于高权限到低权限的情况,一方面因为处理器不需要在TSS中寻找低特权级目标栈的,也就是说TSS也不需要记录3级特权的栈,另一方面因为低权限的栈地址已经存在了,这是由处理器的向高特权级转移指令(int、call等)实现机制决定的。

Reward
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • © 2015-2021 John Doe
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信