进入保护模式
进入保护模式2个例子
第1个例子
;进入保护模式 ,建立段描述符
;在保护模式每一个需要使用的段,都需要建立一个段描述符, 一个段描述符8个字节
;存放段描述符的地方称为全局描述符表GDT
;GDTR 全局描述符寄存器 , 用于指明 GDT 在哪个地址.
;此寄存器共48位, 高32位:GDT起始地址, 低16位:GDT的边界,边界的意思是总字节数-1
;边界的大小:2^16=64k, 每个段描述符8个字节, 意味着最多可以存放8192个段描述符
;lgdt 用于加载GDT, 让CPU知道此处是GDT的起始位置
;lgdt 后需要一个48位的内存单元, 16位GDT界限,高32位GDT起始地址
;段描述符(8字节 64位):
;高32位:
;31 ~ 24 23 22 21 20 19 ~ 16 15 14~13 12 11~8 7 ~ 0
;段地址(31~24) G D/B L AVL 段界限(19~16) P DPL S TYPE 段地址(23~16)
;低32位
; 31 ~ 16 15 ~ 0
;段地址(15-0) 段界限(15 ~ 0)
;G : 段界限粒度. 0 以1字节为单位 , 1 以4K为单位
;S : 描述符类型. 1 代码段/数据段/栈段 , 0 : 系统段
;DPL: 特权 (0,1,2,3)
;P: 此描述符对应的段是否存在于内存中, 有时候对应的段可能会被交换到硬盘上
;D/B: 默认操作数大小,对应栈段和代码段, 0:表示使用16位, 1:使用32位操作数
;对于代码段如果此位是0, 则使用ip, 对于栈,使用sp
;如果是1 , 则使用eip, esp
;L: 64位使用,当前默认0
;TYPE:类别. 给代码段/数据段设置额外属性(读写执行权限)
;AVL:cpu本身不使用
;与实模式不同,描述符的段地址是线性地址,而非逻辑段地址
;段地址: 段的起始地址,实际物理地址(线性地址)
;段界限: 是一个数量单位, 其单位是根据G位来决定,如果G=0,则乘1字节. G=1,则乘4K
;实际的段界限值: (段界限+1)*( 1 或者 4k ) -1;
;1.段界限从0开始计算, 所以+1 得出数量
;2.然后乘以单位 . 根据G位 *1 或 * 4k
;3.由于地址是以0开始, 因此最后-1 得出实际最大偏移值
;总体来说: N个以 (1字节为单位 / 4K为单位) 空间的最大值, 一旦超过即是非法访问
;在保护模式中不再把段地址直接赋值给段寄存器 比如 mov ax, 0x0000 mov ds,ax 这样是不行的
;而是通过段选择子来赋值.
;段选择子是 2个字节长 : 位 15 ~ 3 2 1~0
; 索引 ti RPL ; ti 用于指定是GDT还是LDT , RPL 请求特权
;就目前来说, 只关注索引, 索引: 段描述符在GDT中的位置(索引号)
;由于一个段描述符占8字节, 因此 -> 索引号* 8 = 此段描述符在GDT中的位置(偏移地址)
;具体来说 : 先获取GDTR中高32位的起始地址 + 索引号*8 获取段描述符
;比如目前想把 (伪指令 实模式) mov ds,0xb800
;下面的代码中 0xb8000 是第三个段描述符 , 索引为2, 对应 0x0010
;mov ax,0x0010
;mov ds,ax 就可以了
;[bits 16] (默认值) 以16位来编译机器指令 [bits 32] 32位编译机器指令
MBR_START_ADDR EQU 0x7C00
section mbr align=16 vstart=0
;GDT_BASE 根据实际物理地址计算逻辑段地址和偏移
mov dx,[cs:GDT_BASE + MBR_START_ADDR + 2]
mov ax,[cs:GDT_BASE + MBR_START_ADDR]
mov cx,16
div cx
mov ds,ax ;gdt_base 段地址
mov bx,dx ;gdt_base 偏移地址
;GDT中第一个描述符必须是空描述符
;!描述符8个字节
mov dword [ds:bx],0
mov dword [ds:bx + 0x04], 0
;创建一个代码段描述符
;此段从0x00007c00开始, 总共512字节, 也就是当前的引导程序
;低32位存放段地址,段界限 . 段地址(物理地址低16位):0x7c00 , 段界限: (0x01ff + 1) * 1 -1 = 最大偏移511
mov dword [ds:bx + 0x08], 0x7c0001ff
;S:1 是代码/数据段
;D/B:1 使用32位操作数
;P:1 在内存中
;DPL:0
;G:0 以字节为单位.
;TYPE: 1000 . 可执行,即为代码段
mov dword [ds:bx + 0x0c],0x00409800
;创建数据段, 以便在显存里读写
;段地址:0x000b8000, 段界限:0x0ffff , G=0, 因此实际段界限: (0x0ffff+1)*1-1 = 最大偏移64k
mov dword [ds:bx + 0x10],0x8000ffff
;G:0 以字节为单位
;D/B:1 32位操作数
;S:1 数据段
;DPL:0
;TYPE:0010 可读写,向上扩展
mov dword [ds:bx+0x14],0x0040920b
;创建栈段,向下扩展的段
;段地址:0x00000000, 段界限:0x07a00, G:0, 实际段界限: (0x07a00+1)*1-1+1= 最小偏移:0x7a01;
;栈的段界限指的是 : 最小偏移
;栈是从高地址往低地址, 因此栈顶不设限, esp=0xffff_ffff 可以为最大值
; 跟前2个段的段界限不同, 栈的界限最后需要+1. 原因是栈顶esp指向此栈最高地址的后一个地址上
; 假设目前: mov sp,0x7c00, push ax. 实际的数据存放在0x7BFE,0x7BFF 这2个地址上
; 因此由于初始化 esp 栈顶指向后一个字节上, 所以其界限最后需要 +1 作为最小偏移
mov dword [bx+0x18],0x00007a00
;S:1
;D/B:1 使用32位操作数, 即使用 esp
;DPL:0
;TYPE:0110 可读写,向下扩展
mov dword [bx+0x1c],0x00409600
;设置GDTR , 让CPU知道全局描述符在哪个位置
;设置GDT段界限值 , 界限值是所有段的最后一个字节的偏移 => 目前有4个段描述符,长度:8*4=32字节,最后一个字节:31
mov word [cs:GDT_SIZE + MBR_START_ADDR], 31
;lgdt 读取6个字节. 低16位界限, 高32位GDT起始地址, 目前相当于 0x001f, 0x00007e00
lgdt [cs:GDT_SIZE + MBR_START_ADDR]
;8086只有20根地址线
;打开第21根地址线(A20)
;端口0x92的位1置1即可打开
;0x92端口的位0可以用于重启计算机
in al,0x92
or al,0000_0010b ;位1=1
out 0x92,al ;开启A20
;开始进入保护模式开启动作
;保护模式中bios中断不能再用
cli
;cr0 (32位控制寄存器) 的最后一位PE位 用于开启保护模式
;下面使用了eax, 16位下也可以使用eax
mov eax, cr0
or eax,1 ;把PE位置1
mov cr0,eax ;开启保护模式
;由于当前还是16位模式中, 因此当前的状态是16位保护模式
; 当前还是在16位保护模式下, 需要跳转到32位执行环境中( 也就是对应的段描述符中的D/B位=1)
; 一旦开了保护模式, 所有的段寄存器 必须使用 段选择子
; dword 指明在 16位 运行环境中使用32位操作数 , 会在机器码前加前缀0x66->指明使用32位操作数
; 由于当前已是保护模式, 0x0008 是段选择子, 索引 0001_0000 对应第2个段描述符,其段地址(线性地址)0x7c00
; cs = 根据 GDTR 提供的起始地址 + 索引(1) * 8 获取段描述符
; eip = into_protect_mode
jmp dword 0x0008:into_protect_mode ;dword 来修饰标号(使用4个字节), 否则由于当前是16位环境此标号是2个字节
;* 一个段使用32位还是16位操作数, 具体是由段描述符中的D/B位指定的
;由于段选择子:0x0008 对应的段描述符中D/B=1, 指明此段使用32位操作数
;bits 32 以32位环境编译的机器指令
;以下代码在32位环境中运行, 16位和32位机器码有时候或许一样,但解释不同
;注意 bits 32 是编译环境, 运行环境由 段描述符中的D/B位指定
;如果此段描述符的D/B位如果是0 , 那么将使用16位运行环境 解释 32位机器指令
[bits 32]
into_protect_mode:
;对应的索引号为2, 00000000_0001_0000
;对应上面GDT中第3个段描述符, 此描述符的段地址:0xb8000 指向显存
mov ax,0x0010 ;指向显存 , index = 2 , ti = 0, rpl=0
mov ds,ax ;根据gdtr提供的段起始地址 + (索引) 2 * 8 找到对应描述符并加载到高速缓冲区中
xor ebx,ebx
mov byte [ds:ebx], 'f'
mov byte [ds:ebx + 2], 'u'
mov byte [ds:ebx + 4], 'c'
mov byte [ds:ebx + 6], 'k'
;初始化栈
mov ax, 0x18 ; 11_000B , 索引3 对应上面的栈段描述符
mov ss, ax ;段地址:0x0, 段界限:0x7a01
mov esp, 0x7c00 ;栈顶0x7c00
;当前代码段的D位是1 , 因此默认是32位操作数
;这里使用 byte 只是编译使用, 实际push时还是32位
;栈段中的D/B位也是1, 因此将使用esp-4
push byte 'e'
push byte 'm'
pop eax
mov byte [ds:ebx + 8],al
pop eax
mov byte [ds:ebx + 10],al
hlt
;这2个字段给GDTR(全局描述符表寄存器[48位, 高32位是其起始地址, 低16位 边界]使用)
GDT_SIZE dw 0 ;GDT边界
GDT_BASE dd 0x7e00 ;GDT实际物理地址, mbr起始0x7c00 + 512字节 = 0x7e00
times 510-($-$$) db 0
db 0x55,0xaa
第2个例子
;进入保护模式2
;任何一个段描述符都规定了其界限, 界限本身是一个数量单位, 需根据G位(粒度)来确定其范围
;段界限的计算方式: ( 段界限 + 1 ) * 粒度(1 或者 4k) -1 => 实际段界限
;最后还要加上段基址 : 段基址 + 实际段界限 => 段最大偏移
;代码段和数据段的方向向上扩展. 因此 段基址 + eip <= 段最大偏移
;栈段往下扩展,栈顶不设限最大可以是0xffffffff
;因此栈的最高地址可以是 : 栈基址 + 0xffffffff
;栈的段界限用于限制最小偏移, 实际段界限: (段界限+1)*粒度(1或4k)-1+1 => 实际段界限
;最终:段基址+实际段界限=> 段最小偏移
;合法范围: 段最小偏移 <= (段基址 + esp - 2或4字节) <= 0xffffffff
MBR_START_ADDR EQU 0x7c00
section mbr vstart=0
;在实模式中, 32位CPU上也可以直接使用32位寄存器
;下面的代码基本跟前面的没区别, 除了使用32位寄存器
mov eax,[cs:GDT_BASE + MBR_START_ADDR]
mov ebx, 16
xor edx,edx
div ebx
mov ds, eax ;段地址
mov ebx,edx ;偏移地址
;创建0号描述符
mov dword [ds:ebx],0
mov dword [ds:ebx + 0x04],0
;1号描述符
;数据段
;段地址: 0x0, 段界限(数量):0xfffff, g=1(以4K为单位),type:0010 数据段
;实际段界限:(0xfffff+1)*4k-1 = 4G. 此数据段可以覆盖整个4G空间
mov dword [ds:ebx + 0x08],0x0000ffff
mov dword [ds:ebx + 0x0c],0x00cf9200
;2号描述符
;建立代码段
;段地址:0x7c00, 界限:0x01ff, g=0(以字节为单位)
;实际界限:(0x1ff + 1)*1-1 = 0x01ff 共512字节
;段属性D=1, 使用32位操作数
;该段指向当前引导程序, 即包含16位的代码也包含下面的32位代码
mov dword [ds:ebx + 0x10],0x7c0001ff
mov dword [ds:ebx + 0x14],0x00409800
;3号描述符
;数据段,此数据段对应上面的代码段描述符只修改TYPE
;段地址:0x7c00, 段界限:0x01ff,g=0,D=1
;这个段描述符的作用是可以动态的修改代码段中的代码
;由于无法通过代码段描述符写入数据, 因此可以创建一个对应的数据段描述符,例如调试器通过添加int3(0xCC)来断点
mov dword [ds:ebx + 0x18],0x7c0001ff
mov dword [ds:ebx + 0x1c],0x00409200 ;属性这里只修改了TYPE
;4号描述符
;栈段描述符
;栈段基地址: 0x7c00, 段界限:0xffffe, G=1,TYPE=0110
;实际段界限: (0xffffe+1)*4k-1 + 1 = 0xFFFFF000
;最终最低地址 : 段基址 + 实际段界限 = 0x7c00 + 0xFFFFF000 = 0x100006C00 最高位1被舍去 => 0x6c00
;最高地址: 由于esp最大可以为0XFFFF_FFFF,因此 段基址 + 0xffffffff = 0x100007BFF , 最高位舍去=>0x7bff
;因此栈段空间: 0x7bff - 0x6c00 = 4k
;每当push 2字节/4字节时
;都将检查:1. esp = push - (2或4)
;2. 段基址 + esp >= 段基址 + 实际段界限
;3. 如果越界则有异常
mov dword [ds:ebx + 0x20],0x7c00fffe
mov dword [ds:ebx + 0x24],0x00cf9600
;设置GDTR
mov word [cs:GDT_SIZE + MBR_START_ADDR],39 ;5*8-1
lgdt [cs:GDT_SIZE + MBR_START_ADDR]
;打开A20(21号地址线)
in al,0x92
or al,0x02 ; 0010
out 0x92,al
;屏蔽中断
cli
;设置CR0的 PE位(最后1位)
mov eax, cr0
or eax,1
mov cr0,eax
;-----当前正处于16位保护模式中----------------
;跳转进32位保护模式
;由于当前还处于16位保护模式,想进入32位,需要加载 段描述符并且其描述符的D位=1
;既然要加载段描述符,意味着需要修改段寄存器的值,在保护模式中需要使用段选择子(0x0010)
;dword 使用4字节修饰标号into_protect_mode
jmp dword 0x0010:into_protect_mode
;bits 32 以32位来编译以下代码
[bits 32]
into_protect_mode:
;栈段
;每当push 2/4 字节 会检查时候越界: 段基址 + esp >= 段基址 + 实际段界限
mov eax,0x0020 ;栈段选择子
mov ss,eax ;设置栈段
xor esp,esp ;esp=0, push 4字节后, esp = 0xffff_fffc, 实际地址: 段基址 + esp = 0x7c00 + 0xfffffffc
mov eax,0x08 ;1号描述符,指向4G
mov es,eax
;通过1号描述符 在显存里输出字符
mov byte [es:0xb8000],'f'
mov byte [es:0xb8002],'u'
mov byte [es:0xb8004],'c'
mov byte [es:0xb8006],'k'
;通过1号描述符, 修改当前代码段里的数据
;把前4个字节和后4个字节交换
mov eax,[es:MSG+ MBR_START_ADDR]
mov ebx,[es:MSG+ MBR_START_ADDR + 4]
xchg eax, ebx ;xchg 交换
mov dword [es:MSG+MBR_START_ADDR] , eax
mov dword [es:MSG + MBR_START_ADDR + 4], ebx
;输出代码段中的字符串
mov edx,0xb8008 ;从这开始输出
;由于代码段的type:1000 , 不可读 . 但数据又在代码段中, 此时可用3号描述符的数据段
;3号描述符的段基址和段界限与 代码段一致
mov eax,0x0018 ;3号描述符的段选择子
mov ds,eax
;参数入栈,默认4字节
push dword [ds:MSG_SZIE] ;长度
push es ;目标段选择子
push edx ;目标偏移
push ds ;原段选择子
push MSG ;原偏移
;调用显示
call .show_msg
;add esp, 20 这个不需要了, 在.show_msg中的ret 20 意味着: esp+20
cmp esp,0
jne .done ;检查栈顶是否还原
;使用3号描述继续修改MSG处的数据
mov eax,[ds:MSG]
xchg ah,al
mov dword [ds:MSG], eax
mov eax,dword [ds:MSG_SZIE] ;8
add eax,eax ;16
add edx,eax ;目标偏移往后增加
;调用显示
push dword [ds:MSG_SZIE]
push es
push edx
push ds
push MSG
call .show_msg
.done:
hlt
;参数: 原偏移,原段选择子 ,目标偏移,目标段选择子,字符串长度
;这里由于0xb8000(显存并没有建立段描述符),因此0xb8000作为固定目标偏移来使用, 目标段选择子也固定是1号描述符
;ret 20 ; 20 指的是 参数字节数 * 个数 => 4 * 5 = 20
.show_msg:
push ebp
mov ebp , esp
push es
push ds
push esi
push edi
push eax
push ecx
mov edi,[ebp + 16] ;目标偏移
mov eax,[ebp + 20] ;目标段选择子
mov es,eax
mov eax,[ebp + 12] ;原段选择子
mov ds,eax
mov esi,[ebp + 8] ;原偏移
mov ecx,[ebp + 24] ;字符串长度
.show_loop:
mov al,[ds:esi]
mov byte [es:edi],al
mov byte [es:edi+1], 0x07
add edi,2
inc esi
loop .show_loop
pop ecx
pop eax
pop edi
pop esi
pop ds
pop es
mov esp,ebp
pop ebp
ret 20
;字符串长度
MSG_SZIE dd GDT_SIZE - MSG
;将被修改的数据
MSG db 'fuckme!!'
GDT_SIZE dw 0
GDT_BASE dd 0x7e00 ;实际物理地址
times 510-($-$$) db 0
db 0x55,0xaa