如何用汇编创建一个基础内核

http://www.cnblogs.com/gelosie/archive/2011/03/10/how_to_create_a_basic_kernel_in_assembly_step_one.html

原文是 OSDev Wiki上的一个 Tutorials 文章。

1,第一个启动扇区

2,使用BIOS写消息

3,看看机器码

4,不用BIOS向屏幕打印输出

5,中断

6,进入保护模式

7,非实模式

8,32 位打印输出

附录A,更多资讯

1,第一个启动扇区

 代码

 下面的代码可能是从软盘启动的最小代码的例子。

1 ; boot.asm
2  hang:
3      jmp hang
4      times 512-($-$$) db0

复制代码

 在实模式下,CPU启动,然后BIOS从地址0000:7C00处开始加载代码。在NASM汇编器中,“ times 512 ...”是指

用0填充满512字节。有时候会用另一种表述,在十六进制中 200 等于 十进制中的 512。

经常地,你会在最后看到一个叫启动标识符的东西: 0xAA55。旧版本的 BIOS 通过检查这个标记去差别磁盘上是否

是一个启动扇区。现在,这个是不必要的,若是必要的,最后的代码行会用这个标识符替换 。如下面的版本:

1 ; boot.asm
2  hang:
3     jmp hang
4  
5     times 510-($-$$) db 0 ; 留下最后2字节
6      db 0x55

7     db 0xAA

复制代码

但是,我需要指出的是,当你用这些代码启动,你会看光空白的屏幕同闪烁的光标。你或许会注意到了另外两件事:一个是软驱马达已经关闭,另一个是,你可以按 Ctrl-Alt-Del来重新启动。这儿的重点是,中断(如 0x09 号中断)仍然像平常一般生效。

下面,试试关中断看看发生了什么事情:

1 ;boot.asm
2       cli ; 关中断
3   hang:
4      jmp hang
5  
6      times 510-($-$$) db 0
7      db 0x55
8      db 0xAA

复制代码

你会看到,软驱马达没有关闭,并且不能通过 Ctrl-Alt-Del来进行重启。

如何你移除循环代码,用0填满启动扇区,BIOS对此会给出一些提示。在我的机器上是“Operating System Not Found”。

我换没有尝试用0填满除启动标示符外的启动扇区会是什么情况。

创建磁盘镜像

 上面代码是NASM汇编代码。用partcopy, dd 或者debug得到到软盘。然后你能从这个软件启动

windows

1 nasmw boot.asm -f bin -o boot.bin
2 partcopy boot.bin 0 200 -f0 
3  OR
4 debug boot.bin
5 -W 100 0 0 1

6 -Q  

复制代码



Unix

1 nasm boot.asm -f bin -o boot.bin

2dd if=boot.bin of=/dev/fd0

复制代码

(第一篇END) 

2,使用BIOS写消息

快速回顾:

(1)BIOS加载的启动扇区大小是512字节

(2)在磁盘中启动扇区中的代码被BIOS 加载的地址位于0000:7C00

(3)机器在实模式下启动

(4) 应该清楚:CPU是可以给中断的,除非你明确使用了cli的汇编指令

许多(不是所有)BIOS中断预期是会用实模式下的段值来填充DS。这就是为什么许多BIOS中断在保护模式下

不会起作用的原因。所以当你想使用 int 10h/ah=0eh 去几屏幕打印字符,你需要确保你为打印字符所设置的

seg:offset 的值是正确的。

在实模式下,地址如些计算:  segment*16+offset。 由于偏移地址可以比16大,所以会有许多的 segment offset

对是指向同一地址的。例如:一些时候说加载器是从0000:7C00处开始加载,当然,也有人说是从 07C0:0000处开始

加载。事实上,这两个是同一个地址:  16*0x0000 + 0x7c00 = 16*0x7C0 + 0x0000 = 0x7C00。

无论你使用0000:7C00 还是 07C0:0000 都是可以的,但你当你使用 org 时,你得清楚发生了什么事情。默认地,

原始二进制代码在一开始的偏移是0。若当你需要时,你可以改变这个偏移值,并使它生效。例如,下面的代码段是

从0X7C0处开始 

 1 ; boot.asm     

 2    mov ax, 0x07c0
 3    mov ds, ax
 4  
 5    mov si, msg
 6  ch_loop:lodsb
 7    or al, al
 8    jz hang
 9    mov ah, 0x0E
10    int 0x10
11    jmp ch_loop
12  
13  hang:
14    jmp hang
15  
16 msg   db 'Welcome to Macintosh', 13, 10, 0
17    times 510-($-$$) db 0
18    db 0x55
19    db 0xAA

复制代码

下面是一个 ORG 版本。这次, 从段址0处访问消息 。请留意你仍需要告诉DS怎么设置值:

 1  
 2    org 0x7c00
 3    xor ax, ax ; make it zero
 4     mov ds, ax
 5  
 6    mov si, msg
 7  ch_loop:lodsb
 8    or al, al  ; zero=end of string
 9     jz hang    ; get out
10     mov ah, 0x0E
11    int 0x10
12    jmp ch_loop
13  
14  hang:
15    jmp hang
16  
17 msg   db 'Welcome to Macintosh', 13, 10, 0
18  
19    times 510-($-$$) db 0
20    db 0x55

21    db 0xAA  

复制代码



 过程版本。为了节省空间,典型地使用过程,像下面,在代码中使用 call / ret 

 1    org 0x7c00
 2    xor ax, ax  ;make it zero
 3     mov ds, ax
 4  
 5    mov si, msg
 6    call bios_print
 7  
 8  hang:
 9    jmp hang
10  
11 msg   db 'Welcome to Macintosh', 13, 10, 0
12  
13  bios_print:
14    lodsb
15    or al, al  ;zero=end of str
16     jz done    ;get out
17     mov ah, 0x0E
18    int 0x10
19    jmp bios_print
20  done:
21    ret
22  
23    times 510-($-$$) db 0
24    db 0x55

25    db 0xAA  

复制代码



 由于一些无法解释的原因,在加载SI,然后跳到过程时,我总会遇到bug。幸运的是,可以像我这样在NASM中

使用宏来传递一个参数。

 1 %macro BiosPrint 1
 2                 mov si, word %1
 3  ch_loop:lodsb
 4    or al, al
 5    jz done
 6    mov ah, 0x0E
 7    int 0x10
 8    jmp ch_loop
 9  done:
10 %endmacro
11  
12    org 0x7c00
13    xor ax, ax
14    mov ds, ax
15  
16    BiosPrint msg
17  
18  hang:
19    jmp hang
20  
21 msg   db 'Welcome to Macintosh', 13, 10, 0
22  
23    times 510-($-$$) db 0
24    db 0x55

25    db 0xAA  

复制代码



如果,你觉得代码过长而不好理解,你可以把它分到不同的文件中去,然后在文件中包含在代码开头:

1 jmp main
2  
3 %include "othercode.inc"
4  
5 main:
6    ; ... rest of code here

复制代码



 不要忘记了在一开始就调用 jmp main,不然的话,会顺序启动其它一些随机代码了。

(第二篇END) 

3,看看机器码

上代码:

1 ; nasmw encode.asm -f bin -o encode.bin
2   
3  mov cx, 0xFF
4 times 510-($-$$) db 0
5 db 0x55
6 db 0xAA

复制代码

C:\osdev\debug

encode.bin 

 别把代码写入磁盘。windows用户使用 DEBUG高度, linux用户使用 Hexdump命令。

在“-”之后键入“d”看看二进制文件。("?"查看帮助,"q"退出) 。你会看到一些如下的东西:

 

0AE3:0100 B9 FF 00 00 00 00 etc...

复制代码

 在 http://www.baldwin.cx/386htm/MOV.htm 和 http://www.baldwin.cx/386htm/s17_02.htm查看 mov 指令的 opcode相关介绍。

 也就是说,你在转储中看到的,唯一一个寄存器(CX=1)到在基础 opcode 编码中编码为‘B8’。

但在当你用 ECX替换 CX时,会发生什么事:

1 mov ecx, 0xFF
2 times 510-($-$$) db 0
3 db 0x55
4 db 0xAA


复制代码

 

0AE3:0100 66 B9 FF 00 00 00 00 etc...

复制代码

"66" 是汇编器在默认模式下,以按地址大小覆盖操作数前缀的生成方式。当你使用NASM生成二进制文件时,它是16位格式的。同样,当你使用 bits 指令来改变字长模式时,会生成不同的操作码:

     
1    [BITS 32]  

2    mov cx, 0xFF
3    times 510-($-$$) db 04   db 0x55
5    db 0xAA


复制代码

 这实际上并没有改变处理器的模式,但它确实有助于解决后面的字节序列。

地址

 地址编码有点复杂:

1  mov cx, [temp]
2  
3 temp db 0x99
4    times 510-($-$$) db 0
5    db 0x55
6    db 0xAA

复制代码



0AE3:0100 8B 0E 04 00 99 00 00 00 etc...

“8B”是opcode

"0E"是帮助opcode解释的 Mod R/M  字节

 在 http://www.baldwin.cx/386htm/s17_02.htm 的17.2.1节查看 “ModR/M and SIB Bytes”。

解释,参见图17-2(译注,指上面的连接中的图17-2),可见这个字节是包含有不同的域的,

参照这个图表可以更容易理解这个规则。查看“0E”,你会看到右边标“disp 16”,这是指是

按16位偏移来解释,“0400” 就是这个16位的偏移。若你不清楚为什么是 “0X0004”,这是因为

Intel处理器是小端的。一个数的小端是在前面的。

 “99”就理所当然是在0x0004处字节的修正了。(8B的是0x0000)

有另一种 按地址大小覆盖操作数的前缀是“67”,就是上面的“66”一样。

造成这种现象有许多原因。当我们从16位实模式转入32位保护模式时,我们的代码也将发生变化。

而注意到这种细节,可以减少对这种转换的错误认识 。

(第三篇END) 

4,不用BIOS向屏幕打印字符

我知道这开始看起来像一个不完整的汇编教程,但有,我这样做的背后是有原因的。也就是说,

当进入了保护模式,前面说的许多问题都得到了解决,减少了混乱。

下面这个例子是打印一个字符串和一个内存位置的内容( 这是视频内存中的第一个字符)。其目的是

展示如何在不使用BIOS的情况下以文本的方式向屏幕打印。由于是已经转换到十六进制,因而可以显示。

我们可以检查寄存器和内存值。 堆栈是被包括进来,但闲置没用。

代码:

 1 ;=====================================
 2 ; nasmw boot.asm -f bin -o boot.bin
 3 ; partcopy boot.bin 0 200 -f0
 4  
 5 [ORG 0x7c00]      ; add to offsets
 6     xor ax, ax    ; make it zero
 7     mov ds, ax   ; DS=0
 8     mov ss, ax   ; stack starts at 0
 9     mov sp, 0x9c00   ; 200h past code start
10  
11    mov ax, 0xb800   ; text video memory
12     mov es, ax
13  
14    mov si, msg   ; show text string
15    call sprint
16  
17    mov ax, 0xb800   ; look at video mem
18    mov gs, ax
19    mov bx, 0x0000   ; 'W'=57 attrib=0F
20    mov ax, [gs:bx]
21  
22    mov  word [reg16], ax ;look at register
23    call printreg16
24  
25 hang:
26    jmp hang
27  
28 ;----------------------
29 dochar:   call cprint         ; print one character
30 sprint:   lodsb      ; string char to AL
31    cmp al, 0
32    jne dochar   ; else, we're done
33    add byte [ypos], 1   ;down one row
34    mov byte [xpos], 0   ;back to left
35    ret
36  
37 cprint:   mov ah, 0x0F   ; attrib = white on black
38    mov cx, ax    ; save char/attribute
39    movzx ax, byte [ypos]
40    mov dx, 160   ; 2 bytes (char/attrib)
41    mul dx      ; for 80 columns
42    movzx bx, byte [xpos]
43    shl bx, 1    ; times 2 to skip attrib
44  
45    mov di, 0        ; start of video memory
46    add di, ax      ; add y offset
47    add di, bx      ; add x offset
48  
49    mov ax, cx        ; restore char/attribute
50    stosw              ; write char/attribute
51    add byte [xpos], 1  ; advance to right
52  
53    ret
54  
55 ;------------------------------------
56  
57 printreg16:
58    mov di, outstr16
59    mov ax, [reg16]
60    mov si, hexstr
61    mov cx, 4   ;four places
62 hexloop:
63    rol ax, 4   ;leftmost will
64    mov bx, ax   ; become
65    and bx, 0x0f   ; rightmost
66    mov bl, [si + bx];index into hexstr
67    mov [di], bl
68    inc di
69    dec cx
70    jnz hexloop
71  
72    mov si, outstr16
73    call sprint
74  
75    ret
76  
77 ;------------------------------------
78  
79 xpos   db 0
80 ypos   db 0
81 hexstr   db '0123456789ABCDEF'
82 outstr16   db '0000', 0  ;register value string
83 reg16   dw    0  ; pass values to printreg16
84 msg   db "What are you doing, Dave?", 0
85 times 510-($-$$) db 0
86 db 0x55
87 db 0xAA
88 ;==================================
89


复制代码

(第四篇END)

5,中断

这些代码是为了演示硬件中断是如何产生的,当你按一个键时,他被替换为中断向量表(IVT)中指定的一个值。这通常是指BIOS中的程式。把中断编号乘以4(4是在IVT中每个实体的大小)就可以得到IVT中的实体。这关键的过程只是显示扫描代码,并没有转换为ASCII代码,缓冲或处理扩展键。这样做的原因是不想在这个最简单形式的想法再混入其它内容,除了以最简单的形式提供输入以及输出。我不会更深入到当按下一个键时,如何同为什么从相关的端口读取数据。

我只想说的是,你是同芯片(或者部分芯片)进行交互,而不是做其它一些软件的中介接口。

我个人觉得,最发牢记:无论什么级别的抽象,你最终都要告诉硬盘如何工作。

下面的完整代码中,已经指出键盘是通过端口0X61来进行打开或者关闭。虽然有可能不需要,但得取决于系统。

 1 ;==========================================
 2 ; nasmw boot.asm -f bin -o boot.bin
 3 ; partcopy boot.bin 0 200 -f0
 4  
 5 [ORG 0x7c00]      ; add to offsets
 6     jmp start
 7  
 8    %include "print.inc"
 9  
10  start:   xor ax, ax   ; make it zero
11     mov ds, ax   ; DS=0
12    mov ss, ax   ; stack starts at 0
13    mov sp, 0x9c00   ; 200h past code start
14  
15    mov ax, 0xb800   ; text video memory
16    mov es, ax
17  
18    cli      ;no interruptions
19    mov bx, 0x09   ;hardware interrupt #
20    shl bx, 2   ;multiply by 4
21    xor ax, ax
22    mov gs, ax   ;start of memory
23    mov [gs:bx], word keyhandler
24    mov [gs:bx+2], ds ; segment
25    sti
26  
27    jmp $      ; loop forever
28  
29 keyhandler:
30    in al, 0x60   ; get key data
31    mov bl, al   ; save it
32    mov byte [port60], al
33  
34    in al, 0x61   ; keybrd control
35    mov ah, al
36    or al, 0x80   ; disable bit 7
37    out 0x61, al   ; send it back
38    xchg ah, al   ; get original
39    out 0x61, al   ; send that back
40  
41    mov al, 0x20   ; End of Interrupt
42    out 0x20, al   ;
43  
44    and bl, 0x80   ; key released
45    jnz done   ; don't repeat
46  
47    mov ax, [port60]
48    mov  word [reg16], ax
49    call printreg16
50  
51 done:
52    iret
53  
54 port60   dw 0
55  
56    times 510-($-$$) db 0  ; fill sector w/ 0's
57    dw 0xAA55        ; req'd by some BIOSes
58 ;==========================================

复制代码

 

(第五篇 END)

6,进入保护模式

进入保护模式,事实上只是切换一个特别的控制寄存器(CR0)上的一个位值(所有的其它方式:如A20地址线,任务,IDT,调用门等都是这个的变种。)

当然,切换到保护模式,你必须得使用LGDT指令加载一个称呼为描述符的特别的寄存器(gdtr),去告知过程调用如何访问内存。

我们有争议的是,在这个线程中,GDT是否可以切换到保护模式之后才设定。

  -PypeClicker

复制代码

描述符中的字节位概览:

?

+0 +1 +2 +3  +4 +5 +6 +7

l0     l1 b0  b1   b2 TT    Fl   b3

描述符在内存中的位序列是从底到高的。

0

0x00 lowest byte of Limit

1

0x00 next byte of Limit

2

0x00 lowest byte of Base Addr

3

0x00 next byte of Base Addr

4

0x00 third byte of Base Addr

5

0x00 = (bits) 0 - 00 - 0 - 0000 = P - DPL - S - Type

6

0x00 = (bits) 0 - 0 - 0 - 0 - 0000 = G - D/B - 0 - AVL - Size

7

0x00 fourth and highest byte of Base Addr

类型的位串(第5号字节)

“P”

值为1时,表示段在内存中,访问不存在的段时,会引发异常

“DPL”

描述符特权级(2位),0为最高级,3是最小级

“S”

当是任务状态段(TSS),中断门,陷阱门,任务门,调用门的描述符时,值为1时,其它情况,如代码段数据段堆栈段的描述符时,值为1.

“Tpye”

4位,取决于上面“S”的值情况。当S=0时,表示特定情况下的门。

Type bit 3

当S=1时,是指代码段,否则是指数据段

Type bit 2

 次最高位依赖于最高位。如果代码段,这下一位表示段是否是不是'整合'的。这允许程序在别的地方以没有权限的方式访问这个段,那么这部分是指调用程序的权限级别。如果它是一个数据段,该位指定当段 向上或向下 展开并作为堆栈使用。展开时(位值为0)是你的正常栈的行为。展开式是用于防止堆栈的大小问题。

Type bit 1

这个位是指定读写权限。对于数据段0表示只读的,1是是读写的。当是代码段时,0意味着你不能从中读取(例如使用MOV指令),而 1表示可以。

Type bit 0

值为1时,意味着本段已经被访问,为0时,则没有。

“flags”的位串(第6号字节)

"G"=0时,段大小是以字节为单位,当“G”=1时,指定页大小为4K

“D/B”, 是代码段时, 值为1时,说明操作码同地址码的大小是32位的,值为0时,则是16位的。对于一个数据段来说,值为1时,它意味着堆栈指针是32位的,而值为0时,则为16位的。

“0”这个是保留位,未来Intel可能会使用

“AVL” 是给你使用的。一个疯狂的主意。

“Size” 是指20位的段的前4位。是否是指明最大的段址是1M或者是4G,这个得视信息的粒度而定。

(第六篇END)

7,非实模式

这些代码给出的只是一些小技巧。了解它给出的介绍保护模式的一般概念。当然跳过这些概念,你以后有可能会遇到过一些头疼的东西。

在底部的全局描述符表中的一个描述符是匹配上一篇中所描述的布局的。

在“大小”是1MB,基址是0X0时,你可以自行定义相关的位值。

这样做的原因是在实模式下允许32位的偏移值。当然,你不能超过1M的范围。

在保护模式下,段寄存器的3-15位是描述符表的索引。

这就是为什么在代码中 0X08=1000b,会得到一个实体项。

当寄存器给出了“selector”,一个“段描述符缓冲寄存器”是充满了描述符的值的,包括大小同限制。

当切换回实模式时,这些值不会改变,无论是在哪一个16位的段寄存器中。

所以64K的限制不再生效,可能在实模式下使用32位偏移。

最后:请注意,IP值是对这些有影响的,所以代码本身仍然是限制为64K。

 1 AsmExample:
 2  
 3 ;==========================================
 4 ; nasmw boot.asm -o boot.bin
 5 ; partcopy boot.bin 0 200 -f0
 6  
 7 [ORG 0x7c00]      ; add to offsets
 8  
 9 start:   xor ax, ax   ; make it zero
10    mov ds, ax   ; DS=0
11    mov ss, ax   ; stack starts at 0
12    mov sp, 0x9c00   ; 200h past code start
13  
14    cli      ; no interrupt
15    push ds      ; save real mode
16  
17    lgdt [gdtinfo]   ; load gdt register
18  
19    mov  eax, cr0   ; switch to pmode by
20    or al,1         ; set pmode bit
21    mov  cr0, eax
22  
23    mov  bx, 0x08   ; select descriptor 1
24    mov  ds, bx   ; 8h = 1000b
25  
26    and al,0xFE     ; back to realmode
27    mov  cr0, eax   ; by toggling bit again
28  
29    pop ds      ; get back old segment
30    sti
31  
32    mov bx, 0x0f01   ; attrib/char of smiley
33    mov eax, 0x0b8000 ; note 32 bit offset
34    mov word [ds:eax], bx
35  
36    jmp $      ; loop forever
37  
38 gdtinfo:
39    dw gdt_end - gdt - 1   ;last byte in table
40    dd gdt         ;start of table
41  
42 gdt        dd 0,0  ; entry 0 is always unused
43 flatdesc    db 0xff, 0xff, 0, 0, 0, 10010010b, 11001111b, 0
44 gdt_end:
45  
46    times 510-($-$$) db 0  ; fill sector w/ 0's
47    db 0x55          ; req'd by some BIOSes
48    db 0xAA
49 ;==========================================

复制代码

(第七篇END)

8,32位打印输出

这儿像之前的非BIOS屏幕打印代码例子: AsmExample 。但是,已经为使用32位寄存器和偏移量来实现。一些复杂的字符串指令已经被替换:

 1 ;----------------------
 2 dochar:
 3     call cprint              ; print one character
 4 sprint:
 5     mov eax, [esi]          ; string char to AL
 6     lea esi, [esi+1]
 7     cmp al, 0
 8     jne dochar               ; else, we're done
 9     add byte [ypos], 1       ; down one row
10     mov byte [xpos], 0       ; back to left
11     ret
12  
13 cprint:
14     mov ah, 0x0F             ; attrib = white on black
15     mov ecx, eax             ; save char/attribute
16     movzx eax, byte [ypos]
17     mov edx, 160             ; 2 bytes (char/attrib)
18     mul edx                  ; for 80 columns
19     movzx ebx, byte [xpos]
20     shl ebx, 1               ; times 2 to skip attrib
21  
22     mov edi, 0xb8000         ; start of video memory
23     add edi, eax             ; add y offset
24     add edi, ebx             ; add x offset
25  
26     mov eax, ecx             ; restore char/attribute
27     mov word [ds:edi], ax
28     add byte [xpos], 1       ; advance to right
29  
30     ret
31  
32 ;------------------------------------
33  
34 printreg32:
35     mov edi, outstr32
36     mov eax, [reg32]
37     mov esi, hexstr
38     mov ecx, 8               ; eight nibbles
39  
40 hexloop:
41     rol eax, 4               ; leftmost will
42     mov ebx, eax             ; become rightmost
43     and ebx, 0x0f ;
44     mov bl, [esi + ebx]      ; index into hexstr
45     mov [edi], bl
46     inc edi
47     dec ecx
48     jnz hexloop
49  
50     mov esi, outstr32
51     call sprint
52  
53     ret
54  
55 ;------------------------------------
56  
57 xpos db 0
58 ypos db 0
59 hexstr db '0123456789ABCDEF'
60 outstr32 db '00000000', 0    ; register value
61 reg32 dd 0                   ; pass values to printreg32
62  
63 ;------------------------------------

复制代码

(第八篇END)

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值