linux的汇编学习(3)---进入保护模式

【完整代码已经归档到    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的格式:

   linux的汇编学习(3)---进入保护模式_第1张图片


       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指令:

linux的汇编学习(3)---进入保护模式_第2张图片

     最后一点说明:就是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_start

;进入保护模式之后,我们需要设置代码段将使用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的汇编学习(3)---进入保护模式_第3张图片

你可能感兴趣的:(linux的汇编学习(3)---进入保护模式)