linux的汇编学习(3)---进入保护模式
2017-12-03 23:43
375 查看
【完整代码已经归档到 https://github.com/linzhanglong/mini_bootloader 】
现在我们完成我们第一个主要的功能:引导Linux内核。
首先我们要准备一个Linux内核文件 bzImag。如何引导Linux内核呢?需要做几件事情:
1. 设置GDT,设置访问内存权限;
2. 开启A20地址线(原因:http://blog.csdn.net/ruyanhai/article/details/7181842,是为了兼容历史CPU产生的坑)
3. 进入保护模式
4.把内核加载到内存去;
5.设置启动参数,然后调到内核代码执行。
首先我们先设置CPU进入保护模式,需要做两件事情,第一件事情就是设置GDT。我们看GDT的格式:
GDT用来做什么呢?第一,就是设置我们代码的权限;第二就是设置代码处于CPU哪个级别。其中Type字段决定了类型(数据段还是代码段)以及权限,而DPL决定了属于哪个CPU级别(ring0,还是ring3)。而Base address和Liminit决定了我们这条GDT描述符限制的地址范围。具体各个字段的意思可以参考:https://www.kancloud.cn/digest/protectedmode/121466
说明:S字段我们这里都是设置为1,因为我们处理的是代码段和数据段【统称存储段】。为0表示系统描述度(门描述符,tss描述符等,和我们这里没有关系)
我们现在是引导内核,所以我们这里简单设置两条GDT描述符,一条是ring0的代码段访问权限,一条是ring0的数据段访问权限。范围都是base address = 0,Limit = 0xffffff, G=1(决定Limit的单位),也就是Base_address到Base_address+4G范围。
怎么设置GDT描述符呢?初始化号GDT描述符表,然后把表的大小和内存地址赋值给lgdt指令:
最后一点说明:就是GDT描述符表的第一条描述符必须是空。所以我们现在可以先实现我们的GDT描述符加载函数了,代码如下(enter_protect.asm文件):
现在开始做第二件事,就是启用A20地址,启用的原因也是x86历史的一个坑,具体原因看(https://zh.wikipedia.org/wiki/A20%E6%80%BB%E7%BA%BF),这里不重点关注。我们直接看怎么启动A20,代码如下(enter_protect.asm文件):
现在开始进入保护模式,代码如下(enter_protect.asm文件):
进入保护模式之后,BIOS的中断服务我们就使用不了了。因为BIOS是实模式,我们现在在保护模式下,中断的处理都不一样了。所以我们这里要实现两个函数,第一个函数就是打印字符串函数。这个比较容易实现了,通过操作显卡内存就可以打印了(BIOS在初始化硬件时候已经设置好了VGA现存地址,我们网改地址写入数据时候,显卡硬件就会对应显示数值),代码如下(enter_protect.asm文件):
第二个问题就是在保护模式下打印寄存器的数值,怎么实现呢?就是直接把寄存器的数值转为字符串,让后调用上面的打印字符串的函数就可以了。
例如0x1234先转为"0x1234"然后调用打印字符串就可以实现。这个函数用于我们后面调试使用,很有帮助。(说明:前面实模式下的print_hex也可以使用这个方式重新实现),现在我们直接看保护模式下,打印寄存器的函数实现:
效果图:
现在我们完成我们第一个主要的功能:引导Linux内核。
首先我们要准备一个Linux内核文件 bzImag。如何引导Linux内核呢?需要做几件事情:
1. 设置GDT,设置访问内存权限;
2. 开启A20地址线(原因:http://blog.csdn.net/ruyanhai/article/details/7181842,是为了兼容历史CPU产生的坑)
3. 进入保护模式
4.把内核加载到内存去;
5.设置启动参数,然后调到内核代码执行。
首先我们先设置CPU进入保护模式,需要做两件事情,第一件事情就是设置GDT。我们看GDT的格式:
GDT用来做什么呢?第一,就是设置我们代码的权限;第二就是设置代码处于CPU哪个级别。其中Type字段决定了类型(数据段还是代码段)以及权限,而DPL决定了属于哪个CPU级别(ring0,还是ring3)。而Base address和Liminit决定了我们这条GDT描述符限制的地址范围。具体各个字段的意思可以参考:https://www.kancloud.cn/digest/protectedmode/121466
说明:S字段我们这里都是设置为1,因为我们处理的是代码段和数据段【统称存储段】。为0表示系统描述度(门描述符,tss描述符等,和我们这里没有关系)
我们现在是引导内核,所以我们这里简单设置两条GDT描述符,一条是ring0的代码段访问权限,一条是ring0的数据段访问权限。范围都是base address = 0,Limit = 0xffffff, G=1(决定Limit的单位),也就是Base_address到Base_address+4G范围。
怎么设置GDT描述符呢?初始化号GDT描述符表,然后把表的大小和内存地址赋值给lgdt指令:
最后一点说明:就是GDT描述符表的第一条描述符必须是空。所以我们现在可以先实现我们的GDT描述符加载函数了,代码如下(enter_protect.asm文件):
;***************初始化GDT表**************************** ;第一条是空的描述符 [bits 16] gdt_start: dd 0x0 dd 0x0 ;定义内核代码段访问权限 ;Base_address-Base_address+4G代码段:权限只读,CPU处于ring0级别 gdt_system_cold: dw 0xffff ;Limit dw 0x0 ;Base adderess[15:0] db 0x0 ;Base adderess[23:16] db 10011010b ;P:1->描述符在内存中;DPL:00->ring0,S:1->存储段[代码数据段] 1010->代码段,权限是:执行,可读 db 11001111b ;G:1->Limit单位是4k,D/B:1->使用32位地址,未使用:00,Limit:1111 db 0x0 ;Base address[31:24]:0 ;定义内核数据段访问权限 ;Base_address-Base_address+4G数据段:权限读写,CPU处于ring0级别 gdt_system_data: dw 0xffff ;Limit dw 0x0 ;Base adderess[15:0] db 0x0 ;Base adderess[23:16] db 10010010b ;P:1->描述符在内存中;DPL:00->ring0,S:1->存储段[代码数据段] 010->数据段,权限是:读写 db 11001111b ;Base address[31:24]:0, G:1->Limit单位是4k,D/B:1->使用32位地址,未使用:00,Limit:1111 db 0x0 ;Base address[31:24]:0 gdt_end: dgt_descriptors: dw gdt_end - gdt_start -1 dd gdt_star a9e7 t ;进入保护模式之后,我们需要设置代码段将使用gdt_system_cold描述符,数据段将使用gdt_system_data描述符 GDT_SYSTEMCOLD_OFFSET equ gdt_system_cold - gdt_start ;在描述符表的偏移位置 GDT_SYSTEMDATA_OFFSET equ gdt_system_data - gdt_start ;在描述符表的偏移位置 ;向外提供的函数,初始化GDT描述符表 init_gdt: pusha lgdt [dgt_descriptors] popa ret
现在开始做第二件事,就是启用A20地址,启用的原因也是x86历史的一个坑,具体原因看(https://zh.wikipedia.org/wiki/A20%E6%80%BB%E7%BA%BF),这里不重点关注。我们直接看怎么启动A20,代码如下(enter_protect.asm文件):
;******************启用A20************* ;摘自 http://mrhopehub.github.io/2014/12/26/enabling-the-A20-Gate.html EnableA20_KB: push ax ;Saves AX mov al, 0xdd ;Look at the command list out 0x64, al ;Command Register pop ax ;Restore's AX ret
现在开始进入保护模式,代码如下(enter_protect.asm文件):
;*****************设置保护模式**************************** enter_protect: cli call init_gdt call EnableA20_KB mov eax, cr0 or eax, 0x1 mov cr0, eax jmp GDT_SYSTEMCOLD_OFFSET:init_protect ;开始进入保护模式,都是32位地址 ;下面的函数都是32位的函数 [bits 32] init_protect: ;现在设置我们的数据段使用GDT_SYSTEMDATA_OFFSET描述符 mov ax, GDT_SYSTEMDATA_OFFSET mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax ;设置堆栈s mov ebp, 0x90000 mov esp, ebp mov ebx, MSG_ENTER_PROTECT_OK call print_string_protect mov ebx, 0x123456 call print_hex_pm jmp $ ret MSG_ENTER_PROTECT_OK db 'Enter protect mode ok', 0
进入保护模式之后,BIOS的中断服务我们就使用不了了。因为BIOS是实模式,我们现在在保护模式下,中断的处理都不一样了。所以我们这里要实现两个函数,第一个函数就是打印字符串函数。这个比较容易实现了,通过操作显卡内存就可以打印了(BIOS在初始化硬件时候已经设置好了VGA现存地址,我们网改地址写入数据时候,显卡硬件就会对应显示数值),代码如下(enter_protect.asm文件):
;***************保护模式下的打印,不能通过BIOS,但是可以通过显卡映射的内存来操作***** [bits 32] VIDEO_MEMERY_START equ 0xb8000 ;@ brief 保护模式下的打印字符串 ;@ param ebx 字符串的地址 print_string_protect: pusha ;先读取当前光标位置 ;光标位置高8位 xor eax, eax mov dx, 3D4H mov al, 0xE out dx, al ;设置索引:读取高8位 mov dx, 3D5H in al, dx ;从VGA寄存器读取数据 mov ah, al ;光标位置低8位 mov al, 0xF mov dx, 3D4H out dx, al ;设置索引:读取低8位 mov dx, 3D5H in al, dx ;从VGA寄存器读取数据 ;设置光标对应的显卡内存位置,两个字节对应一个字符 mov edx, VIDEO_MEMERY_START add edx, eax add edx, eax ;保存光标的位置 mov ecx, eax print_next_protect: mov al, [ebx] ;显示的字符 ;判断是否为结束符 cmp al ,0 je print_ok_protect mov ah, 0x0f ;显示的字符颜色和背景设置 mov [edx], ax add ebx, 1 add edx, 2 inc ecx ;更新光标位置 jmp print_next_protect print_ok_protect: ;设置光标显示到下一行,每一行有80个字符 ;下一行的位置 = (当前的位置 + 79) / 80 * 80 ;ecx 保存要设置的坐标位置 add ecx, 79 mov eax, ecx mov ebx, 80 xor edx,edx div ebx mul ebx mov ecx, eax mov dx, 3D4H mov al, 0xE out dx, al ;设置索引:读取高8位 mov dx, 3D5H mov al, ch out dx, al ;从VGA寄存器写入数据 ;光标位置低8位 mov al, 0xF mov dx, 3D4H out dx, al ;设置索引:读取低8位 mov dx, 3D5H mov al, cl out dx, al ;从VGA寄存器读取数据 jmp $ popa ret
第二个问题就是在保护模式下打印寄存器的数值,怎么实现呢?就是直接把寄存器的数值转为字符串,让后调用上面的打印字符串的函数就可以了。
例如0x1234先转为"0x1234"然后调用打印字符串就可以实现。这个函数用于我们后面调试使用,很有帮助。(说明:前面实模式下的print_hex也可以使用这个方式重新实现),现在我们直接看保护模式下,打印寄存器的函数实现:
;保护模式下打印ebx寄存器的数值 ;这里把数值转为字符串,然后调用print_string_protect即可 [bits 32] init_print_hex_pm: pusha mov ah, 0x0f mov al, '0' mov [edx], ax add edx, 1 mov al, 'x' mov [edx], ax popa add edx, 1 ret ;@ brief 直接把bl转为字符对应的ASCII码,例如3->'3',10->'a' convert_bx_hex2str_pm: ;如果数字大于15,异常。打印?号 cmp bl, 15 jg _ERROR_pm ;如果数字大于10,那么转为A-F cmp bl, 10 jge _LETTER_pm ;如果数字是0-10,就只加上'0',就可以把0-10转为对应数字的ascii码 add bl, '0' jmp _EXIT_pm _LETTER_pm: ;先把数字减去10,然后加上A,就可以把10-15的数字转为A-F sub bl, 10 add bl, 'A' jmp _EXIT_pm _ERROR_pm: mov bl, '?' _EXIT_pm: ret ;@ brief 把一个数字转为多位十六进制输出 ;@ param bx保存要打印的数字 print_hex_pm: pusha ;首先通过掩码和移位的方式,把数字以十六进制的方式从最后一位开始一个个压入堆栈 ;然后输出时候才从堆栈里一个个取出来,这样就可以实现顺序打印 xor cx, cx _NEED_PUSH_pm: ;取掩码获取数字以十六进制形式的最后一位,例如数字0x1234取出4,其中4保存到al,0x123保存到bx。cl计数 mov eax, ebx and eax, 0x0f shr ebx, 4 ;入栈并且计数 push ax inc cl ;如果bx数值为0,表示我们已经全部处理完 cmp ebx, 0 jne _NEED_PUSH_pm mov eax,CONVERT_HEX2str add eax, 2 ;开始出栈,并且显示 _NEED_POP_pm: pop bx call convert_bx_hex2str_pm mov [eax], bx inc eax dec cl cmp cl, 0 jne _NEED_POP_pm ;开始打印 mov ebx, CONVERT_HEX2str call print_string_protect popa jmp $ ret ;8个字节,还有一个结束符 CONVERT_HEX2str: db '0','x',0,0,0,0,0,0,0,0,0
效果图:
相关文章推荐
- Linux学习笔记----CentOS 在什么情况下,进入图形界面安装模式 ??
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.6 中断和异常处理
- <<Linux内核完全剖析 --基于0.12内核>> 学习笔记 第4章 80x86保护模式及其编程 4.1 80x86系统寄存器和系统指令
- 从linux0.11学习linux内核设计之模式转换:实模式-保护模式(1)
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.5 保护
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.3 分段机制
- 汇编学习(十六)X86汇编学习 (3) 32位保护模式杂谈
- linux-0.11调试教程,显示strat minix后不能进入保护模式的原因,看bochsout.txt
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.7 任务管理
- 汇编学习(14)保护模式和实模式下的间接操作数
- NASM 纯汇编打造简单中文操作系统(2 init.asm初始化操作系统内存。进入32位保护模式)
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.8 保护模式编程初始化
- Linux内核0.12——从一个简单的多任务内核实例学习保护模式
- [Intel汇编-NASM]进入保护模式之前的准备
- [Intel汇编-NASM]进入保护模式全过程
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.4 分页机制
- [Intel汇编-NASM]进入保护模式之前的准备
- 从linux0.11学习linux内核设计之模式转换:实模式-保护模式(2)
- [Intel汇编-NASM]进入保护模式全过程
- NASM 纯汇编打造简单中文操作系统(2 init.asm初始化操作系统内存。进入32位保护模式)