Linux操作系统, 构建自己的内核——5. 利用C语言绘制操作系统图像界面

课程链接:
利用C语言绘制操作系统图像界面

课程的目的是在屏幕上绘制一个图形界面,其实只需要往显存对应的缓冲区写入表示颜色的字符就行了。循环写入显然是一个不错的办法,用汇编写一个循环?一般人的汇编水平可能并不太够。所以作者提供的办法是先用c语言写,再反汇编成汇编代码。

1. kernel.asm

%include "pm.inc"

org   0x9000

VRAM_ADDRESS  equ  0x000a0000

jmp   LABEL_BEGIN

[SECTION .gdt]
 ;                                  段基址          段界限                属性
LABEL_GDT:          Descriptor        0,            0,                   0  
LABEL_DESC_CODE32:  Descriptor        0,      SegCode32Len - 1,       DA_C + DA_32
LABEL_DESC_VIDEO:   Descriptor        0B8000h,         0ffffh,            DA_DRW
LABEL_DESC_VRAM:    Descriptor        0,         0ffffffffh,            DA_DRW
LABEL_DESC_STACK:   Descriptor        0,             TopOfStack,        DA_DRWA+DA_32

GdtLen     equ    $ - LABEL_GDT
GdtPtr     dw     GdtLen - 1
           dd     0

SelectorCode32    equ   LABEL_DESC_CODE32 -  LABEL_GDT
SelectorVideo     equ   LABEL_DESC_VIDEO  -  LABEL_GDT
SelectorStack     equ   LABEL_DESC_STACK  -  LABEL_GDT
SelectorVram      equ   LABEL_DESC_VRAM   -  LABEL_GDT


[SECTION  .s16]
[BITS  16]
LABEL_BEGIN:
     mov   ax, cs
     mov   ds, ax
     mov   es, ax
     mov   ss, ax
     mov   sp, 0100h

     mov   al, 0x13
     mov   ah, 0
     int   0x10

     xor   eax, eax
     mov   ax,  cs
     shl   eax, 4
     add   eax, LABEL_SEG_CODE32
     mov   word [LABEL_DESC_CODE32 + 2], ax
     shr   eax, 16
     mov   byte [LABEL_DESC_CODE32 + 4], al
     mov   byte [LABEL_DESC_CODE32 + 7], ah

     xor   eax, eax
     mov   ax,  cs
     shl   eax, 4
     add   eax, LABEL_STACK
     mov   word [LABEL_DESC_STACK + 2], ax
     shr   eax, 16
     mov   byte [LABEL_DESC_STACK + 4], al
     mov   byte [LABEL_DESC_STACK + 7], ah

     xor   eax, eax
     mov   ax, ds
     shl   eax, 4
     add   eax,  LABEL_GDT
     mov   dword  [GdtPtr + 2], eax

     lgdt  [GdtPtr]

     cli   ;关中断

     in    al,  92h
     or    al,  00000010b
     out   92h, al

     mov   eax, cr0
     or    eax , 1
     mov   cr0, eax

     jmp   dword  SelectorCode32: 0

     [SECTION .s32]
     [BITS  32]
     LABEL_SEG_CODE32:
     ;initialize stack for c code
     mov  ax, SelectorStack
     mov  ss, ax
     mov  esp, TopOfStack

     mov  ax, SelectorVram
     mov  ds,  ax

C_CODE_ENTRY:
     %include "write_vga.asm"


    io_hlt:  ;void io_hlt(void);
      HLT
      RET

SegCode32Len   equ  $ - LABEL_SEG_CODE32

[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512  db 0
TopOfStack  equ  $ - LABEL_STACK

针对以上代码,自己的一些理解:

1) LABEL_DESC_VIDEO的定义是多余的,代码并未用到这个段。那么LABEL_DESC_VIDEO对应的起始地址0B8000h与write_vga.c中使用的0x000a0000有啥区别呢?

A0000~AFFFF: VGA图形模式显存空间
B0000~B7FFF: MDA单色字符模式显存空间
B8000~BFFFF: CGA彩色字符模式显存空间
C0000~C7FFF: 显卡ROM空间(后来被改造成多种用途,也可以映射显存)
C8000~FFFFE: 留给BIOS以及其它硬件使用(比如硬盘ROM之类的)
参考:
https://www.zhihu.com/question/269649445/answer/351632444

2) GDT表中LABEL_DESC_CODE32的作用?

LABEL_DESC_CODE32和前面的课程一样,都是为了跳转到代码处去执行。其实有一个疑问,在关中断前后的这段代码中:

     cli   ;关中断

     in    al,  92h
     or    al,  00000010b
     out   92h, al

     mov   eax, cr0
     or    eax , 1
     mov   cr0, eax

     jmp   dword  SelectorCode32: 0

     [SECTION .s32]
     [BITS  32]
     LABEL_SEG_CODE32:
     ;initialize stack for c code
     mov  ax, SelectorStack

jmp命令后面紧接着的就是想要执行的LABEL_SEG_CODE32对应的代码,直接顺序执行不就行了吗?还要jmp命令干啥。我的理解是,CS寄存器和IP寄存器决定了cpu将要执行的指令的位置。但是在实模式与保护模式下的寻址方式是不同的。
实模式寻址方式:CS寄存器*16+IP寄存器
保护模式寻址方式:CS寄存器存储的实际上是段选择符,找到GDT表中对应的表项,然后找到段基址+IP寄存器(段内偏移)
也就是说,在CS寄存器和IP寄存器不发生改变的情况下,从实模式切换到保护模式,寻址得到的地址也不一样。所以说,切换成保护模式之后,需要使用jmp模式,修改CS寄存器和IP寄存器的值,才能定位到想要执行的代码处。

3) GDT表中 LABEL_DESC_STACK的作用?

栈是程序运行的过程中需要的一段内存,那么这段内存就必须得提前规划好,并写入GDT表中,用SS寄存器来寻址。栈在工作的过程中,需要3个寄存器:
SS寄存器:存储栈地址对应的段选择符。
EBP寄存器:存储栈底在段中的偏移。
ESP寄存器:存储栈顶在段中的偏移。
所以,必须要提前申请一段内存,放入GDT表中,并修改SS寄存器的值,使其指向GDT表中对应的位置。所以栈的大小是有限的,如果超出了栈的大小,就是造成堆栈溢出的错误。
栈是设计成向下增长的,所以mov esp, TopOfStack将esp设置为申请的内存段的最高地址(段内偏移),此时ebp还不需要设置值,因为ebp的作用其实就是在esp移动的时候保存esp的值。
因为程序中只申请了512字节的栈空间,如果使用超过512字节的空间:

char buf[513];

会导致程序发生致命错误,无法运行。
在这里插入图片描述

4) LABEL_DESC_VRAM的作用?

从代码可以看出,这个地址是最终被放到了ds寄存器中:

     mov  ax, SelectorVram
     mov  ds,  ax

而且,LABEL_DESC_VRAM的段基地址是0,段长度是ffff ffffh,也就是4G,也就是整个线性地址空间。
为什么要这么做呢?原来,我们在c代码中,直接操作了地址a0000地址附近的一段空间,而且这个地址,我们希望访问的就是对应的线性地址,那就只能把段基地址设置为0。而且段长度一定要大于要访问的最大地址。将分配的这段内存的段基地址写入到ds寄存器中,程序就可以访问这段内存了。这样,程序其实可以操作整个4G的空间,所以程序自身一定要保证不要操作到其他代码段使用的地址,否则将发生不可预知的错误。
可以做一个小例子来验证一下以上理解:
如果修改段长度到小于0xaffff,程序将发生错误:
LABEL_DESC_VRAM: Descriptor 0, 0afffeh, DA_DRW
段长度到大于等于0xaffff,程序则可以正常运行。
LABEL_DESC_VRAM: Descriptor 0, 0affffh, DA_DRW

感兴趣的同学可以尝试一下。
LABEL_DESC_VRAM: Descriptor 0, 0a0fffh, DA_DRW
的效果:
在这里插入图片描述

参考:寄存器中cs,ds,ss,es的区别

5) ax, al, ah, eax的关系?

EAX is the full 32-bit value
AX is the lower 16-bits
AL is the lower 8 bits
AH is the bits 8 through 15 (zero-based)
RAX, which hold a 64-bit value, and where EAX is mapped to the lower 32 bits.

参考:https://stackoverflow.com/questions/15191178/how-do-ax-ah-al-map-onto-eax

2. write_vga.asm

; Disassembly of file: write_vga.o
; Sat Sep  7 21:55:43 2019
; Mode: 32 bits
; Syntax: YASM/NASM
; Instruction set: 80386


;;;global CMain: function

;;;extern io_hlt                                           ; near


;;;SECTION .text   align=1 execute                         ; section number 1, code

CMain:  ; Function begin
        push    ebp                                     ; 0000 _ 55
        mov     ebp, esp                                ; 0001 _ 89. E5
        sub     esp, 24                                 ; 0003 _ 83. EC, 18
        mov     dword [ebp-0CH], 0                      ; 0006 _ C7. 45, F4, 00000000
        mov     dword [ebp-10H], 655360                 ; 000D _ C7. 45, F0, 000A0000
        jmp     ?_002                                   ; 0014 _ EB, 14

?_001:  mov     eax, dword [ebp-10H]                    ; 0016 _ 8B. 45, F0
        mov     dword [ebp-0CH], eax                    ; 0019 _ 89. 45, F4
        mov     eax, dword [ebp-10H]                    ; 001C _ 8B. 45, F0
        mov     edx, eax                                ; 001F _ 89. C2
        mov     eax, dword [ebp-0CH]                    ; 0021 _ 8B. 45, F4
        mov     byte [eax], dl                          ; 0024 _ 88. 10
        add     dword [ebp-10H], 1                      ; 0026 _ 83. 45, F0, 01
?_002:  cmp     dword [ebp-10H], 720895                 ; 002A _ 81. 7D, F0, 000AFFFF
        jle     ?_001                                   ; 0031 _ 7E, E3
?_003:  call    io_hlt                                  ; 0033 _ E8, FFFFFFFC(rel)
        jmp     ?_003                                   ; 0038 _ EB, F9
; CMain End of function


;;;SECTION .data   align=1 noexecute                       ; section number 2, data


;;;SECTION .bss    align=1 noexecute                       ; section number 3, bss

一个分号的是反编译之后自带的注释,三个分号的是手工添加的注释。
之前基本还没研究过汇编代码,所以当看到一段c代码对应的汇编代码,并读懂的时候,觉得很激动,很神奇。
说说对这段代码的理解。

        push    ebp                                     ; 0000 _ 55
        mov     ebp, esp                                ; 0001 _ 89. E5
        sub     esp, 24                                 ; 0003 _ 83. EC, 18

每个函数执行开始前的标准范式,将ebp寄存器保存起来。
并将esp保存到ebp中。
然后将esp减去24,表示需要24个字节的空间。但是不太理解,这个24是怎么计算出来的。
也没明白,后面的ebp-10H和ebp-0CH是怎么计算出来的。

ebp-0CH表示的是变量p。
ebp-10H表示的是变量i。变量i被赋初值655360(0xa0000)。

?_001:  mov     eax, dword [ebp-10H]                    ; 0016 _ 8B. 45, F0
        mov     dword [ebp-0CH], eax                    ; 0019 _ 89. 45, F4

表示p=i。

        mov     eax, dword [ebp-10H]                    ; 001C _ 8B. 45, F0
        mov     edx, eax                                ; 001F _ 89. C2

表示将i的值存到edx中。

        mov     eax, dword [ebp-0CH]                    ; 0021 _ 8B. 45, F4
        mov     byte [eax], dl  

则表示将i的低8位写入到位置i。
要访问位置i对应的内存,cpu需要进行一次寻址。ds段寄存器的基地址被我们在前面设置成了0,所以i就对应着线性地址。

?_002:  cmp     dword [ebp-10H], 720895                 ; 002A _ 81. 7D, F0, 000AFFFF
        jle     ?_001   

一次循环完之后进行一次判断,决定是否需要继续循环。

__x86.get_pc_thunk.ax与Invalid operand type error

通过objconv反编译出的代码一直包含着这个函数。
然后使用nasm编译kernel.asm时一直报错:
Invalid operand type error
对比作者的代码,发现我反汇编出的代码多了__x86.get_pc_thunk.ax这个函数。在网上搜了一下,原来这个函数是32位 机器特有的一个指令,作用貌似是获取下一条指令的地址,并设置指令寄存器为对应的值。
我编译的环境确实是32位的debian。
所以切换到64位的机器下进行操作,编译出的代码便是正常的。__x86.get_pc_thunk.ax相关的原理与机制,未来得及深入研究。

Invalid operand type error
i386 Linux下Elf动态链接分析
What is __i686.get_pc_thunk.bx? Why do we need this call?

3.效果

*p = i & 0x0f;修改为*p = i & 0xff;,画出来的效果有一些不一样:
在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值