学习自狄泰软件
利用选择子 拿到对应的段描述符,之后自然就可以拿到段基址,再加上段内偏移地址,就可以跳转
dword 双字 32位
word 两个字节 16位
dword 四个字节 32位
问题:为什么要明确的标识出 dword 32位呢?
答案:因为我们现在的无条件跳转 jmp 是从16位代码段 跳转到 32位代码段,16位代码段的意思就是 编译器在默认的情况下 对16位代码段里面的立即数 解释为16位。编译器有默认的准则,如果不明确指定,则在16位代码段中,编译器会默认 认为这个立即数就是16位的,多的部分就扔掉了。
如下代码段,在从16位代码段 跳转到 32位代码段的时候,没有明确的给编译器指定 dword !!,那么编译器就会按照16位代码段默认准则 即16位 进行编译,看到立即数,就按照16位的方式解释它,如果该立即数大过16位数据能够表示的范围,就会截断,所以这里 编译器就会认为跳转到 32位代码段 偏移地址为 0x5678的地方,1234 舍弃。
所以 dword 就是告诉编译器,现在要从16位跳转到 32位,因此解释这一行语句的时候,用 32位的方式来解释 !!!
显卡的主要任务是 提供要显示的数据,并且还有一个额外的智能:控制显示器的工作状态。当显卡控制完显示器,并提供好数据之后,显示器就可以把数据显示出来了。
显卡所提供的数据是放在现存中的,显存用来存储需要显示的数据。在本质上 和内存无差别,只不过显存是存在于显卡内部的。
所以直接向这个地址范围内写数据,显存当中的内容就会发生改变,对应的屏幕当中的内容也会发生改变。
自定义函数 需要一个栈空间,就是一段内存空间,这段内存空间在保护模式下 是需要被定义的,通过段描述符定义,所以可以在全局段描述符表中 新增一个 段描述符 ,代表全局的栈段。用于保护模式下的函数调用。
而我们需要打印的字符串可以放到 数据段,以往的学习表明 数据段就是 全局的一段数据空间,该空间存放数据,其中字符串也是放到全局数据段中的\,并且是放到全局只读数据段。 而这个所谓的全局数据段 本质就是一块内存,既然是一块内存,那就应该用段描述符定义一下,所以可以在 全局段描述符表中添加一个 段描述符,即数据段描述符
需要注意的是:80386以及之后的处理器 的16位实模式下,是可以使用32位寄存器 32位地址的,80386以及之后的处理器 虽然上电之后处于 8086的实模式,但是这是为了兼容8086 而 故意使用的 16位运行模式,即此时 对应的 立即数 或者寄存器 都使用16位,但这是为了兼容,实际80386以及之后的处理器 的16位实模式下 是可以使用32位寄存器和地址的!!!
loader.asm
%include "inc.asm"
org 0x9000
jmp CODE16_SEGMENT
[section .gdt]
; GDT definition
; 段基址, 段界限, 段属性
GDT_ENTRY : Descriptor 0, 0, 0
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32
;显存段描述符
;显存段属性:DA_DRWA + DA_32 --> 32位保护模式下的段,并且该段 可读可写 并且在这一刻之前 显存已经被访问过了(因为之前BIOS需要打印的信息 都是通过操作显存 显卡完成的)
VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32
;全局数据段 段描述符
;全局数据段属性 :32位保护模式下 只读段
DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32
;全局栈空间 段描述符
;全局栈空间属性 :32位保护模式下 可读可写段 范围 0 - 0x7c00 定义为32位保护模式下全局栈空间
STACK_DESC : Descriptor 0, TopOfStackInit, DA_DRW + DA_32
; GDT end
GdtLen equ $ - GDT_ENTRY
GdtPtr:
dw GdtLen - 1
dd 0
; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0
;定义 显存段描述符VIDEO_DESC 的选择子,在全局段描述符表中的下标是2 所以段描述符索引值是 2
;属性是 SA_TIG(表示该选择子想要去访问的段描述符是 全局段描述符) + SA_RPL0(特权级 第0特权级)
VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0
;定义 全局数据段 段描述符DATA32_DESC的 选择子,在全局段描述符表中的下标是3 所以段描述符索引值是 3
;属性是 SA_TIG(表示该选择子想要去访问的段描述符是 全局段描述符) + SA_RPL0(特权级 第0特权级)
Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0
;定义 栈空间 段描述符STACK_DESC的 选择子,在全局段描述符表中的下标是4 所以段描述符索引值是 4
StackSelector equ (0x0004 << 3) + SA_TIG + SA_RPL0
; end of [section .gdt]
; 定义常量 初始栈指针位置
TopOfStackInit equ 0x7c00
;定义 32位保护模式 数据段,存放字符串
[section .dat]
[bits 32]
DATA32_SEGMENT:
;定义需要打印的字符串 以0结束
DTOS db "D.T.OS!", 0
;字符串的长度,此时代表字符串的 段内偏移地址
DTOS_OFFSET equ DTOS - $$
HELLO_WORLD db "Hello World!", 0
HELLO_WORLD_OFFSET equ HELLO_WORLD - $$
;数据段长度
Data32SegLen equ $ - DATA32_SEGMENT
[section .s16]
[bits 16]
CODE16_SEGMENT:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
;在16位 代码段中 设置了栈指针
mov sp, TopOfStackInit
; initialize GDT for 32 bits code segment
;设置 CODE32_SEGMENT段 32位段基址 到 对应的段描述符对应位置
mov esi, CODE32_SEGMENT
mov edi, CODE32_DESC
call InitDescItem
;设置 数据段 32位段基址 到 数据段 段描述符对应位置
mov esi, DATA32_SEGMENT
mov edi, DATA32_DESC
call InitDescItem
; initialize GDT pointer struct
mov eax, 0
mov ax, ds
shl eax, 4
add eax, GDT_ENTRY
mov dword [GdtPtr + 2], eax
; 1. load GDT
lgdt [GdtPtr]
; 2. close interrupt
cli
; 3. open A20
in al, 0x92
or al, 00000010b
out 0x92, al
; 4. enter protect mode
mov eax, cr0
or eax, 0x01
mov cr0, eax
; 5. jump to 32 bits code
jmp dword Code32Selector : 0
;初始化段描述符中的 段基址, 根据代码段标签初始化对应的段描述符
; esi --> code segment label 代码段标签
; edi --> descriptor label 段描述符标签
InitDescItem:
;备份 eax
push eax
; 设置32位段基址 到 段描述符对应位置
mov eax, 0
mov ax, cs
;eax 左移4位
shl eax, 4
;代码段标签
;段寄存器<<4) + (32位代码段 偏移地址 == 32位代码段的真实物理地址,即段基地址
add eax, esi
;将 ax寄存器保存的值 放到 段描述符标签+2 地址处, 段基址的0-15位 放到 段描述符的16-31 位
mov word [edi + 2], ax
;eax 右移16位 也就是低16位被移出去了
shr eax, 16
; al(ax低8位) 此时al是32位代码段基地址当中的第三个字节(段基址 16-23位) 放到 段描述符的32-39位
mov byte [edi + 4], al
; ah(ax高8位) 此时ah是32位代码段基地址当中第四个字节(段基址 24-31位) 放到 段描述符的55-63位 第七个字节
mov byte [edi + 7], ah
;回复 eax
pop eax
ret
[section .s32]
[bits 32]
CODE32_SEGMENT:
;首先分别将
;显存段 基地址放到 gs段寄存器中,gs段寄存器用于存放 显存段基地址
;栈空间段 基地址放到 ss段寄存器中,ss段寄存器用于存放 栈空间段基地址
;数据空间段 基地址放到 ds段寄存器中,ds段寄存器用于存放 数据段基地址
; 将显存段描述符的选择子 放到 gs寄存器中,所以此时gs寄存器就代表了这整个显存段
mov ax, VideoSelector
mov gs, ax
;通过栈空间段描述符的选择子 获取段的起始地址 设置到 ss
mov ax, StackSelector
mov ss, ax
;通过段描述符的选择子 获取段的起始地址 即ds
;再通过具体的段内偏移地址ebp , 得到具体地址 ds:ebp
;通过段描述符的选择子 设置 对应段寄存器,即获取数据段 段地址
mov ax, Data32Selector
mov ds, ax
;获取目标字符串在段内偏移地址, 因为是保护模式下的编程,地址用段内偏移地址
mov ebp, DTOS_OFFSET
;打印属性 黑底红字
mov bx, 0x0C
;打印位置
mov dh, 12
mov dl, 33
call PrintString
mov ebp, HELLO_WORLD_OFFSET
mov bx, 0x0C
mov dh, 13
mov dl, 31
call PrintString
jmp $
; 32位保护模式下下的 打印函数 需要定义在 [section .s32] 代码节下
; ds:ebp --> string address 目标字符串地址
; bx --> attribute 打印属性
; dx --> dh : row, dl : col 打印坐标
PrintString:
push ebp
push eax
push edi
push cx
push dx
print:
;取当前字符
mov cl, [ds:ebp]
;判断当前字符是否为0
cmp cl, 0
;如果是0 则打印结束 跳转到 end标签
je end
;如果不为0
; 行*80,相乘结果放到 eax寄存器中
mov eax, 80
mul dh
;加列坐标 dl --> eax+列
add al, dl
; eax*2 得到最终的打印位置
shl eax, 1
;将打印位置 放到 edi 中
mov edi, eax
;获取字符属性 - 高八位 需要显示字符的颜色
mov ah, bl
;获取字符属性 - 低八位 需要显示的字符
mov al, cl
;将字符写入显存中 打印
mov [gs:edi], ax
;下一个字符
inc ebp
;下一个字符打印的位置
inc dl
;无条件跳转 循环
jmp print
end:
pop dx
pop cx
pop edi
pop eax
pop ebp
ret
Code32SegLen equ $ - CODE32_SEGMENT