您的位置:首页 > 其它

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

2012-01-12 14:24 246 查看
/article/5460227.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

7
复制代码

不要忘记了在一开始就调用 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

7
复制代码
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.htmhttp://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)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: