一开始的内存寻址就是单纯在一个物理的线性地址空间直接寻址,后来,技术发展了,内存大,搞出了内存分段机制。技术又发展了,空间更大了,就想搞多用户多任务操作系统。多用户多任务系统需要保证安全,不能出现—我是a,但我能直接修改b,c,d,e,f的数据,或者说,a程序可以直接读取b,c,d,e,f程序运行时的数据。从硬件层面提供一种策略—保护模式。
保护模式下寻址的核心数据结构:段描述符,段选择子
跨平台段描述符可视化工具(基于浏览器的工具,从某种意义上来讲,可不就是跨平台_ _):
工具位置:
https://gitee.com/YMQ_1314/linux_os_learn
描述符工具在util/gdt/gdt_generate.html
段选择子工具在util/gdt/segment_selector.html
一、内存分段机制
注:
1、我这里所讲的内存分段机制,不是在分页机制基础之上的分段存储管理方式。
2、讲得是16位模式下的寻址
1、内存分段机制的定义
内存分段(英语:Memory segmentation),一种电脑内存的管理技术,它将电脑的主内存分成许多区
段(segment或sections)。当处理器要进行内存寻址时,会使用一个数值,这个数值包括了某个区
段,以及偏移量(offset)。
2、内存分段机制下的寻址及其举例
! as86 ld86 汇编 链接
! 段寄存器 cs代码段寄存器,ds数据段寄存器,es附加段寄存器,ss堆栈段寄存器
! 物理线性地址 = 段寄存器中的值*16 + 偏移地址。
! 下边打印字符串 ds:si其具体地址就是 ds中的值*16 + si中的值(十进制,即我们数! 学中的1,2,3,4.....)。
! 假设在十六进制下, ds数据段 = 0x9000, si = 0x100,
! 那么其物理线性地址为0x90000 + 0x100 = 0x90100。 段地址无符号左移四位 + 偏移地址
INITSEG = 0x9000
mov ax,#SETUPSEG
mov ds,ax
mov es,ax
call print_enter_wrap ! 打印回车换行
sub si,si
lea si,extend_memeory_size_title
call print_string
! 显示位于DS:SI处以NULL(0x00)结尾的字符串。
print_string:
lodsb
and al,al
jz fin
call print_char ! 显示al中的一个字符。
jmp print_string
fin:
ret
! 该子程序使用中断0x10的OxOE功能,以电传方式在屏幕上写一个字符。光标会自动移到下一个
! 位置处。如果写完一行光标就会移动到下一行开始处。如果已经写完一屏最后-行,则整个屏幕
! 会向上滚动一行。字符0x07 (BEL)、0x08 (BS)、OxOA(LF)和OxOD (CR)被作为命令不会显示。
! 输入:AL——欲写字符;BH——显示页号;BL——前景显示色(图形方式时)。
print_char:
push ax
push cx
mov bh,#0x00 !显示页面
mov cx,#0x01
mov ah,#0x0e
int 0x10
pop cx
pop ax
ret
extend_memeory_size_title:
.ascii "extend memeory size: "
db 0x00
二、保护模式及其寻址
百度百科定义:
保护模式,是一种80286系列和之后的x86兼容CPU操作模式。保护模式有一些新的特色,设计用来增强多工和系统稳定
度,像是 内存保护,分页 系统,以及硬件支援的 虚拟内存。大部分的现今 x86 操作系统 都在保护模式下运行,包
含 Linux、FreeBSD、以及 微软 Windows 2.0 和之后版本。
另外一种286和其之后CPU的运行模式是实模式,一种向前兼容且关闭了保护模式这些特性的CPU运行模式。用来让新的
芯片可以运行旧的软件。依照设计的规格,所有的x86 CPU都是在实模式下开机,来确保传统操作系统的向前兼容性。
在任何保护模式的特性可用前,他们必须要由某些程序手动地切换到保护模式。在现今的计算机,这种切换通常是由操
作系统在开机时候必须完成的第一件任务的一个。它也可能当CPU在保护模式下运行时,使用虚拟86模式来运行设计运
行在实模式下的代码。
保护模式与实模式相对应。在80286以前,CPU只有实时模式,地址总线有20位,而内存地址是16位,也就是最多能够
访问2^20=1M的内存空间。在80286及以后,内存地址改为16位或32位,至少可以访问到2^32=4G的内存空间。但为了
保证后续的CPU能够运行旧的CPU,只能保持向下兼容。因此,80286及以后的CPU首先进入实模式,然后通过切换机制
再进入到保护模式。
在保护模式下,当我们执行一个装载CS寄存器的指令的时候,段选择子(Segment Selector)被装入CS寄存器的可
见部分,同时CPU根据此选择子到相应的描述符表中(GDT或LDT)找到相应的段描述符并将其内容装载入CS寄存器的不
可见部分。随后CPU当需要通过CS的内容进行地址运算的时候,也仅仅引用不可见部分。
注:
1、讲得是32位模式下的寻址。
2、32位机器为保护模式。
3、64位机器为IA-32e模式。
4、16位寻址模式。即:寻址能力为1M;分段机制;每段不超过64kb。
5、x86处理器在内存中按小端顺序存放和检索数据,假设16进制数据0x12345678,那么
从0000号位置开始存放,数据越高位的内容放在内存编号越小的位置。
在16位模式下,程序可以自由地访问不允许它的内存位置,而且最大的寻址空间为1MB。可随着科技的发展,
硬件更新,有了32位机器。此时,它的寻址空间至少达到了4GB。寻址空间的增大,也为多用户多任务系统
的实现带来了可能,同时也产生了访问安全性的问题。
32位机也采用了段+偏移的模式来寻址。但与实模型不同的是,由于地址线和数据线宽度一致,因而,每个段
最大可以到4G,并且段基址也是32位的无需进行左移处理。不过此时的段寄存器存放的是段选择子。寻址时,
根据段寄存器里边的段选择子找到段描述符(里边存放段基地址),取出段基地址,最后将其与偏移地址相加
得到最终要访问地物理地址。这一套逻辑增加了操作空间,那么访问安全也是可以解决的—操作空间大,
再增加一套逻辑也无所谓。
在保护模式下,对内存的访问,仍然使用段地址和偏移地址,但是每个段在访问之前,必须先登记。
登记的信息包括段的起始地址,段的界限和段的各种访问属性等。
假设你要访问的段跟你的程序不符合,就会被阻止。也就是说操作系统底层部分,不会被恶意访问或更改。
核心部分:
GDT(全局描述符表)
GDTR(48位,全局描述符表寄存器)
0~15--全局描述符表边界 16~47--全局描述符表线性地址
32位地址部分保存的是全局描述符表在内存中的起始线性地址。
比如下边代码:
lgdt [cs: pgdt+0x7c00]
pgdt dw 0 ;16位,2字节,存界限
dd 0x00007e00 ;GDT的物理地址 32位,4字节,存起始线性地址
它就表明我要在物理线性地址0x00007e00处放置全局描述符表。
为啥要加0x7c00?
pgdt是在我当前这个程序里边。
假设我的程序在0地址处,那么就应该是 lgdt [cs:pgdt]; 但是现在我的程序在
0x7c00处,那么就应该是 lgdt [cs: pgdt+0x7c00]。
16位的边界部分保存的是全局描述符表的界限,其在数值上等于表的大小(总字节数)减一。
换句话说,全局描述符表的界限值就是表内最后一个字节(也是表内最后一个描述符的第8个字节)
的偏移量,第一个字节的偏移量是0。
比如下边代码:
;初始化描述符表寄存器GDTR
mov word [cs: pgdt+0x7c00],39 ;描述符表的界限
lgdt [cs: pgdt+0x7c00]
因为GDTR的界限是16位,所以该表最大是2的16次方个字节,也就是65536(64K)个字节,又因为
一个描述符占8个字节,故最多可定义8192个描述符。
段描述符表项:
这里的数据对应底层存储的数据。x86处理器在内存中按小端顺序存放和检索数据,即数据的低位字节存放在内存
的低地址中,高位字节存放在内存的高地址中。
展示数据时:
在前面的叫高位字节,在后面的叫低位字节,就跟数学里边的数一样---最小的个位在最右边。
存储数据时:
内存低地址: 内存地址小的。
内存高地址: 内存地址大的。
比如:定义了一个数据
hex_data dd 0x12345678
这里的1234叫高位,5678叫低位
那么按x86小端顺序,其底层存储的数据为
78 56 34 12
低位在内存低地址,高位在内存高地址
由于最小存储单元为字节,所以一个字节里边的存储顺序不会改变。
这两个图片中数据对应小端顺序存储的数据。
我的段基地址为: 0x00107c00
那么按照小端顺序,7c00应该在内存低四字节里边,0010在内存高四字节里边。
段基地址31~24 : 00
段基地址23~16 : 10
段基地址15~0 : 7c00
// 低四字节 === 低位
mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF
// 高四字节 === 高位
mov dword [ebx+0x24],0x0040920b ;粒度为字节
// 连起来
0x0040920b80007fff
0b0000000001000000100100100000101110000000000000000111111111111111
0100 (G D/B L AVL) 4
G:0(字节) D/B:1(32位) L:0(否) AVL:0(用户自定义位--不用管)
假设为1010
G:1(4KB) D/B:0(16位) L:1(64位) AVL:0(用户自定义位--不用管)
0000 (段界限符19-16) 0
1001 (P DPL S) 9
P:1(段位于内存中) DPL:00(0特权级系统 1 2 3用于普通程序) S:1(代码段与数据段)
假设为0110
P:1(段位于硬盘中) DPL:11(3特权级系统 1 2 3用于普通程序) S:0(系统段)
0010 (子类型type X E/C W/R A) 2
X: 0(数据段) E: 0(向上扩展--向高地址扩展) W:1(可以正常读写) A:0(未标记为访问)
假设为1010
X:1(代码段) C:0(非非依从段,即该代码段只能从与它特权级相同的代码段调用,或者通过门调用)
R:1(可读) A:0(未标记为访问)
假设为1110
X:1(代码段) C:0(允许从低特权级的程序转移到该段执行)
R:1(可读) A:0(未标记为访问)
0000 (段基地址23-16) 0
在物理内存的顺序:
FF 7F 00 80 0B 92 40 00
TYPE字段共有4位,用于指示描述符的子类型。
对于数据段,E位指示段的扩展方向。E=0表示向上扩展,即向高地址方向扩展;E=1时表示向下扩展,即向低地址
方向扩展。W位表示是否可写。若W=0表示段是不允许被写入的,否则会触发CPU异常中断,若W=1则表示段是可以
正常读写的。
对于代码段,C位表示该段是否是特权级依从。C=0表示非依从段,即该代码段只能从与它特权级相同的代码段调
用,或者通过门调用;C=1则表示允许从低特权级的程序转移到该段执行。R位表示该代码段是否可读。
A位表示该段最近是否被访问过,每当该段被访问时,CPU会自动将该位置置1。
S位用于表示描述符的类型。当S=0时,表示这是一个系统段;当S=1时,表示这是一个代码段或数据段。
1bit
DPL位表示描述符的特权级(Descriptor Privilege Level,DPL)。这两位用于指定对应段的特权
级,即0、1、2、3。其中0级为最高特权级别,3级为最低特权级别。刚进入保护模式时执行的代码具有
最高的特权级别。通常0级用于操作系统的代码,3级用于普通用户程序的代码。有些CPU指令只能由0特
权级的程序来执行。2bit
P表示段存在位(Segment Present)。P位用于指示描述符所对应的段是否存在。当P=0时,表示段位于硬盘中;当P=1时,表示段位于内存中。1bit
AVL位是用户自定义位,通常由操作系统来使用。1bit
L位是64位代码段标志,为1表示是64位。1bit
D/B位表示默认操作数大小。当为0时表示是16位的,当为1时表示是32位的。1bit
G代表段的粒度。
若G=0,则表示段界限以字节为单位,此时20位的段界限可以表示从1字节到1MB的范围;
若G=1,则表示段界限以4KB为单位,此时20位的段界限可以表示从4KB到4GB的范围。
描述符生成工具:
https://gitee.com/YMQ_1314/linux_os_learn
位置:util/gdt/gdt_generate.html
段选择子
它是描述符表项在描述符表中的段标识符。可以通过它来找到段描述符表项,从而得到段基地址。此时,只需要跟偏移地址相加,就能得出具体的物理地址。
段选择子长16位。
段选择子的高13位是描述符索引(Index).所谓描述符索引
结构如下:
|段描述符表项索引值(index)|TI|RPL|
RPL(占2位,0-1):请求特权级。
0、1、2、3。其中0级为最高特权级别,3级为最低特权级别。刚进入保护模式时执行的代码具有最高的特权级别。通常0级用于操作系统的代码,3级用于普通用户程序的代码。有些CPU指令只能由0特权级的程序来执行。
特权级就是一个辅助的东西,为了保证底层安全而提供的辅助信息。
我确定你可以访问,你才能访问,那么底层肯定就是对比。
比如:
在保护模式下,我们又是基于描述符表,这里就会有“描述符特权级”。
程序当前执行位置,肯定也会有“当前特权级”。还有其他,毕竟对比的
前提是你得有。
网络上一个待验证的说法(https://blog.csdn.net/Gyc8787/article/details/121879012):
首先,将控制直接转移到非依从的代码段,要求当前特权级CPL和请求特权级RPL都等于目标代码段描述符的DPL。即,在数值上,
CPL=目标代码段描述符的DPL
RPL=目标代码段描述符的DPL
一个典型的例子就是使用jmp指令进行控制转移:
jmp 0x0012:0x00002000
因为两个代码段的特权级相同,故,转移后当前特权级不变。
其次,要将控制直接转移到依从的代码段,要求当前特权级CPL和请求特权级RPL都低于,或者和目标代码段描述符的DPL相同。即,在数值上,
CPL≥目标代码段描述符的DPL
RPL≥目标代码段描述符的DPL
控制转移后,当前特权级保持不变。
第三,高特权级别的程序可以访问低特权级别的数据段,但低特权级别的程序不能访问高特权级别的数据段。访问数据段之前,肯定要对段寄存器DS、ES、FS和GS进行修改,比如
mov fs,ax
在这个时候,要求当前特权级CPL和请求特权级RPL都必须高于,或者和目标数据段描述符的DPL相同。即,在数值上,
CPL≤目标数据段描述符的DPL
RPL≤目标数据段描述符的DPL
最后,处理器要求,在任何时候,栈段的特权级别必须和当前特权级CPL相同。因此,随着程序的执行,要对段寄存器ss的内容进行修改时,必须进行特权级检查。以下就是一个修改段寄存器ss的例子:
mov ss,ax,
在对段寄存器ss进行修改时,要求当前特权级CPL和请求特权级RPL必须等于目标栈段描述符的DPL。即,在数值上,
CPL=目标栈段描述符的DPL
RPL=目标栈段描述符的DPL
0特权级是最高的特权级别,当一个系统的各个部分都位于0特权级时,各种特权级检查总能够获得通过,就像这种检查和检验并不存在一样。所以,处理器的设计者建议,如果不需要使用特权机制的话,可以将所有程序的特权级别都设置为0。
TI(占1位,2):引用描述符表指示位。
TI=0表示从全局描述符表GDT中读取描述符表项;
TI=1表示从局部描述符表LDT中读取描述符表项。
index(占32位,3-15):描述符索引是指描述符在描述符表中的序号(从0开始)。
我定义了如下段描述符表项:
;跳过0#号描述符的槽位
;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xFFFFF
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
;创建保护模式下初始代码段描述符
mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,界限0x1FF
mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
;建立保护模式下的堆栈段描述符 ;基地址为0x00007C00,界限0xFFFFE
mov dword [ebx+0x18],0x7c00fffe ;粒度为4KB
mov dword [ebx+0x1c],0x00cf9600
;建立保护模式下的显示缓冲区描述符
mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF
mov dword [ebx+0x24],0x0040920b ;粒度为字节
段选择子为0x0010:
jmp dword 0x0010:flush ;16位的描述符选择子32位偏移
展开: 0b 0000 0000 0001 0000
特权级: 0b00
TI: 0b0,从GDT表中读取
段描述符索引: 0b 0000 0000 0001 0 = 2(上边的代码段)
这个0#号描述符的槽位则为 0, 一般来说下标都是从0开始的。
数据段索引:0b 0000 0000 0000 1,加上TI跟特权级,整个为0x0008
段选择子工具
位置:util/gdt/segment_selector.html
效果:
段描述符、段选择子使用例子
; nasm汇编代码
; jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移
; 这一句 ---- 就是32位寻址过程(段选择子找段描述符,取段基地址与偏移地址相加)。
mov ax,cs
mov ss,ax
mov sp,0x7c00 ;设置堆栈段和栈指针
;计算GDT所在的逻辑段地址
mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址
xor edx,edx
mov ebx,16
div ebx ;分解成16位逻辑地址
mov ds,eax ;令DS指向该段以进行操作
mov ebx,edx ;段内起始偏移地址
;跳过0#号描述符的槽位
;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xFFFFF
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
;创建保护模式下初始代码段描述符
mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,界限0x1FF
mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
;建立保护模式下的堆栈段描述符 ;基地址为0x00007C00,界限0xFFFFE
mov dword [ebx+0x18],0x7c00fffe ;粒度为4KB
mov dword [ebx+0x1c],0x00cf9600
;建立保护模式下的显示缓冲区描述符
mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF
mov dword [ebx+0x24],0x0040920b ;粒度为字节
;初始化描述符表寄存器GDTR
mov word [cs: pgdt+0x7c00],39 ;描述符表的界限
lgdt [cs: pgdt+0x7c00]
in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20
; 当CPU进入保护模式之后,原有的中断向量表不再适用。因此在重新设置保护模式下的中断环境之前,
; 必须关中断。
cli ;中断机制尚未工作
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位
;以下进入保护模式... ...
jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移
[bits 32] ;清流水线并串行化处理器
flush:
mov eax,0x0008 ;加载数据段(0~4GB)选择子
mov ds,eax
mov eax,0x0018 ;加载堆栈选择子
mov ss,eax
xor esp,esp ;堆栈指针 <- 0
...
ghalt:
hlt
;----------------------------------------------------------------------------
core_base_address equ 0x00040000 ;常数,内核加载的起始内存地址
core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号
;-------------------------------------------------------------------------------
pgdt dw 0
dd 0x00007e00 ;GDT的物理地址
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa