操作系统学习常见疑惑问与答[编码实践部分]
—问题1:nasm:org指令深入理解
作者:yxin1322
blog:http://blog.csdn.net/yxin1322 转载请注明出处
大三的时候已经学过了《操作系统原理》这门课,虽然学习过程中做过一些实验,但对操作系统的认识仍然停留在理论的层面上,对于如何才能编程实现一个真正的操作系统缺乏可操作的方案,于是有了我现在进行的学习计划。按照计划,近期我读了一些有关操作系统编写的书籍,也参考了网上很多的文章,接触了不少国内这方面的论坛,总算对编写操作系统有了一个大概的认识。在这期间,我曾经被许多小的问题困扰过,并且我认为许多初学操作系统的同学也会跟我一样有相同的困惑,所以我将自己找到的答案和心得以问答的形式记录在此,希望能给后来者提供参考,同时也当作自己的学习备忘。
学习还在继续,问题也在不断增加,所以我会不断丰富问答的内容。其中部分回答摘自网上的文章,我会注明,而大部分回答则是我自己的见解。如果大家读后认为回答有不对或理解偏差的地方,还请留言指正。
需要说明的是我这里讨论的操作系统是指基于IA构架80386以上CPU的操作系统,并不包括其他计算机构架之上的操作系统或嵌入式操作系统。
问:为什么编写NASM语法的系统引导程序,汇编代码的开始总是使用“org 7c00h”?为什么有时候去掉org指令程序也能正常执行?
答:对于这个问题,我首先在《NASM中文手册》中找到了org指令的解释:NASM汇编编译器为bin文件格式提供了额外的操作符org,它的功能是指定程序被载入内存时的起始地址。根据书中的解释,我们很容易想到,因为引导程序将会被加载到内存0x7c00处,而且引导程序一般都被编译成bin文件格式(bin文件格式没有文件头,它的文件映像与加载到内存运行时的内存映像是一致的),似乎在引导程序中用 org 7c00h是很符合规范的,可仔细一想,似乎又不对,按《NASM中文手册》的说法,使用org 7c00h将会指定程序加载入内存的起始地址为7c00h,但我们都知道,引导程序加载到内存的7c00h处是一项标准,并不是在编程时决定的,经过试验也验证了我的想法,将org后的数字改成其他值,bois程序一样将它加载到7c00处。那么是不是可以去掉org指令呢,因为程序被加载到哪里并不关它什么事。于是我将org指令去掉,重新编译,写入软盘引导扇区,用它引导系统。出乎预料,程序不能正常运行——没有正常打印出提示信息!到底怎么回事呢?我决定一探究竟。以下是我的引导代码:
boot.asm
1 %include "PrintLib.inc"
2
3 org 07c00h
4 ;org 0100h
5 mov ax,cs
6 mov ds,ax
7 mov es,ax
8
9 mov ah,10h
10 mov al,03h
11 mov bl,01h
12 int 10h
13
14 PrintString BootMessage,LenOfBootMessage,display_mode_2,0h,(ATTR_BLACK<<4)|ATTR_GREEN,0000h
15
16 hlt ;停机
17
18
19 BootMessage:db "Dreamix Starting Please wait..."
20 LenOfBootMessageequ $-BootMessage
21
22 times 510-($-$) db 0
23 dw0xaa55
boot.asm 里引用到了PrintLib.inc文件,PrintLib.inc文件中定义了一个向屏幕输出字符串信息的宏,封装了部分10h BISO子功能,文件内容如下:
PrintLib.inc
1 %ifndef PrintLib
2 %define PrintLib
3
4 ;此宏在实模式下使用,属于BIOS子功能调用
5
6 ;显示模式
7 %define display_mode_1 00h ;字符串只包含字符码,显示之后不更新光标位置,属性值在BL中
8 %define display_mode_2 01h ;字符串只包含字符码,显示之后更新光标位置,属性值在BL中
9 %define display_mode_3 02h ;字符串包含字符码及其属性值,显示之后不更新光标位置
10 %define display_mode_4 03h ;字符串包含字符码及其属性值,显示之后更新光标位置
11
12 ;背景及字体格式属性值
13 %define ATTR_BLACK 0h
14 %define ATTR_BLUE 01h
15 %define ATTR_GREEN 02H
16 %define ATTR_PURPLE 03h
17 %define ATTR_RED 04h
18 %define ATTR_MAGENTA 05h
19 %define ATTR_BROWN 06h
20 %define ATTR_GREYISH 07h
21 %define ATTR_GREY 08h
22 %define ATTR_LIGHTBLUE09h
23 %define ATTR_LIGHTGREEN0Ah
24 %define ATTR_LIGHTPURPLE0Bh
25 %define ATTR_LIGHTRED0Ch
26 %define ATTR_LIGHTMAGENTA0Dh
27 %define ATTR_YELLOW 0Eh
28 %define ATTR_WHITE0Fh
29
30 ;参数: 1.要显示的字符串标号 2.要显示的字符串的长度值
31 ;3.显示模式
32 ;4.视频页号
33 ;5.当显示模式选3和4时为0h,否则需要背景和字符的格式属性值
34 ;6.显示的列和行
35 %macro PrintString 6
36
37 push ax
38 push bp
39 push cx
40 push bx
41 push dx
42
43 mov ax,%1
44 mov bp,ax
45 mov cx,%2
46 mov ax,01300h + %3
47 mov bx,%4 + %5
48 mov dx,%6
49 int 10h
50
51 pop dx
52 pop bx
53 pop cx
54 pop bp
55 pop ax
56 %endmacro
57
58 %endif
以上两个文件中的代码都是正确的代码,其中的boot.asm文件使用了org 7c00h.当我把org 7c00h语句去掉后,编译的引导程序不能正常打印提示信息"Dreamix Starting Please wait...".
用WinHex查看编译好的boot.bin文件,显示如下:
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 8C C8 8E D8 8E C0 B4 10 B0 03 B3 01 CD 10 50 55 ŒÈ?Ø?À´.°.³.Í.PU
00000010 51 53 52 B8 2C 7C 89 C5 B9 1F 00 B8 01 13 BB 02 QSR¸,|‰Å¹..¸..».
00000020 00 BA 00 00 CD 10 5A 5B 59 5D 58 F4 44 72 65 61 .º..Í.Z[Y]XôDrea
00000030 6D 69 78 20 53 74 61 72 74 69 6E 67 20 50 6C 65 mix Starting Ple
00000040 61 73 65 20 77 61 69 74 2E 2E 2E 00 00 00 00 00 ase wait........
00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA ..............Uª
可以看到字符串被编译到文件头偏移02Ch处,此时文件映像的内容也反映了它在内存中的映像内容,当boot.bin被加载到内存7c00h处时,字符串"Dreamix Starting Please wait..."应该位于内存7c2ch处。我们用Bochs调试未加org 7c00语句的引导程序,看看它到底是如何运行的。将boot写入软盘镜像文件,把该镜像文件挂载为Bochs虚拟机的软驱,启动Bochs调试器,先用pb命令在物理内存0x7c00处下断点,然后用c命令执行执行到断点(关于如何用Bochs调试操作系统请读者自己查阅相关资料,此处不再赘述),此时boot.bin已经被加载到内存0x7c00处。用disassemble命令反汇编这段内存,得到如下结果:
<bochs:3> disassemble 0x7c00 0x7c30
00007c00: ( ): mov ax, cs ; 8cc8
00007c02: ( ): mov ds, ax ; 8ed8
00007c04: ( ): mov es, ax ; 8ec0
00007c06: ( ): mov ah, 0x10 ; b410
00007c08: ( ): mov al, 0x3 ; b003
00007c0a: ( ): mov bl, 0x1 ; b301
00007c0c: ( ): int 0x10 ; cd10
00007c0e: ( ): push ax ; 50
00007c0f: ( ): push bp ; 55
00007c10: ( ): push cx ; 51
00007c11: ( ): push bx ; 53
00007c12: ( ): push dx ; 52
00007c13: ( ): mov ax, 0x2c ; b82c00
00007c16: ( ): mov bp, ax ; 89c5
00007c18: ( ): mov cx, 0x1f ; b91f00
00007c1b: ( ): mov ax, 0x1301 ; b80113
00007c1e: ( ): mov bx, 0x2 ; bb0200
00007c21: ( ): mov dx, 0x0 ; ba0000
00007c24: ( ): int 0x10 ; cd10
00007c26: ( ): pop dx ; 5a
00007c27: ( ): pop bx ; 5b
00007c28: ( ): pop cx ; 59
00007c29: ( ): pop bp ; 5d
00007c2a: ( ): pop ax ; 58
00007c2b: ( ): hlt ; f4
00007c2c: ( ): inc sp ; 44
00007c2d: ( ): jb .+0x7c94 ; 7265
00007c2f: ( ): popa ; 61
实际上可执行的代码只到00007c2b,后面的是数据和填充字符,我们看到,红色标出的两条指令负责在调用10h中断之前将需要打印的字符串的首地址装入约定的寄存器bp,它装入的是字符串相对于7c00的偏移0x2c。我们知道,实模式下内存的寻址是通过段寄存器提供的段基址和偏移地址相组合的方式,这里的0x2c相当于偏移地址。那么段基址是多少呢?我们在Bochs下用info registers命令查看,得到以下结果:
<bochs:7> info registers
eax 0xfffaa55 268413525
ecx 0xa0001 655361
edx 0x0 0
ebx 0x0 0
esp 0xfffe 0xfffe
ebp 0x0 0x0
esi 0xa070 41072
edi 0xffde 65502
eip 0x7c00 0x7c00
eflags 0x82 130
cs 0x0 0
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
一般来说bp是进行堆栈寻址的,我不知道此时bp是和哪个段寄存器配合寻址,但我们发现所有6个段寄存器的值都是0,因此无论如何(即使引导程序一开始就将cs寄存器的内容复制到了es和ds),用bp里的偏移0x2c和段基址0x0只能访问到内存单元0x2c,而不是0x7c2c(字符串"Dreamix Starting Please wait..."被加载大内存0x7c2c处),这就是为什么程序不能正确打印提示字符的原因。受原来编程习惯的影响,我们想当然的认为段寄存器应该存着程序被加载到的段的基址,也就是0x7c00,这样和偏移地址0x2c结合刚好可以正确找到字符串,但事实是引导程序被加载后,所有的段寄存器都被清零了,不知道这是不是IBM PC兼容机启动时的规范动作?知道的朋友可以给我留言。此时,由于段寄存器的值为0x0,偏移地址就变成了绝对(物理)地址,要访问内存0x7c2c,偏移地址就必须是0x7c2c;或者编程时将段寄存器的值赋为0x7c00,偏移地址0x2c保持不变,也可以达到预期目的。
我们来看看boot.asm中加入org 7c00h指令后的情况,按前述步骤在Bochs中反汇编代码,结果如下:
<bochs:5> disassemble 0x7c00 0x7c30
00007c00: ( ): mov ax, cs ; 8cc8
00007c02: ( ): mov ds, ax ; 8ed8
00007c04: ( ): mov es, ax ; 8ec0
00007c06: ( ): mov ah, 0x10 ; b410
00007c08: ( ): mov al, 0x3 ; b003
00007c0a: ( ): mov bl, 0x1 ; b301
00007c0c: ( ): int 0x10 ; cd10
00007c0e: ( ): push ax ; 50
00007c0f: ( ): push bp ; 55
00007c10: ( ): push cx ; 51
00007c11: ( ): push bx ; 53
00007c12: ( ): push dx ; 52
00007c13: ( ): mov ax, 0x7c2c ; b82c7c
00007c16: ( ): mov bp, ax ; 89c5
00007c18: ( ): mov cx, 0x1f ; b91f00
00007c1b: ( ): mov ax, 0x1301 ; b80113
00007c1e: ( ): mov bx, 0x2 ; bb0200
00007c21: ( ): mov dx, 0x0 ; ba0000
00007c24: ( ): int 0x10 ; cd10
00007c26: ( ): pop dx ; 5a
00007c27: ( ): pop bx ; 5b
00007c28: ( ): pop cx ; 59
00007c29: ( ): pop bp ; 5d
00007c2a: ( ): pop ax ; 58
00007c2b: ( ): hlt ; f4
00007c2c: ( ): inc sp ; 44
00007c2d: ( ): jb .+0x7c94 ; 7265
00007c2f: ( ): popa ; 61
可以看到,加入org 7c00h后,偏移被编译成了0x7c2c,再次用info registers命令查看寄存器内容,发现还是全为0x0,因此程序通过基址加偏移的方式能够正确访问到字符串。
通过以上分析,我们可以看出org指令的作用确实是指示出程序将要被加载到内存的起始地址,这里用“指示”比用原来的“指定”更确切点。“指示”有被动的含义,org指令本身并不能决定程序将要加载到内存的什么位置,它只是告诉编译器,我的程序在编译好后需要加载到xxx地址,所以请你在编译时帮我调整好数据访问时的地址。用“指定”有种主动的含义,容易引起误解。另外我们看出,org指令只会在编译期影响到内存寻址指令的编译(编译器会把所有程序用到的段内偏移地址自动加上org后跟的数值),而其自身并不会被编译成机器码。
到此我们已经初步了解到了org指令的作用,一句话,就是为程序中所有的内部地址引用增加一个段内偏移值(引导程序可以看做是被加载到以0为基址的段,偏移为0x7c00)。
为了方便调试,我们常常用NASM将程序编译为windows能直接执行的com文件,这样能在windows下直接运行观看其效果。其实com文件格式和bin文件格式并没有本质的区别,它们都是纯二进制文件,即文件映像的内容和内存映像的内容相同。所不同的是,com文件总是被MS的操作系统加载到段偏移0100h处,因此com的汇编源文件中需要加入语句org 100h。
下面我们来看看com文件的寻址情况,我们分别用windows自带的debug和boland的turbo debuger 调试运行com版本的boot程序,分析其中的异同:
图1: Debug下调试执行boot.com
图2: Turbo Debuger下调试执行boot.com
从图1和图2中可以看出,访问字符串的偏移地址都是012ch,即org指令指定的100h与字符串文件内偏移相加而得到的值。而两种情况下段寄存器的值都不为0,一个是0BE2,一个是5328。为什么不同的调试器在加载同一个com文件时,加载到的段各不相同;而同一个调试器每次加载同一文件总在一个段,这其中有什么规则,我不得而知,请知道的朋友留言告诉我。我们看到,虽然两种情况下的段基址各不相同,但用基址+偏移的方式总能正确访问到内存数据,这归功于程序总被加载到段偏移0100h处,由此引出了以下结论:
如果一个程序使用了org xxx指令,那么该程序只能被加载到段内偏移xxx处,否则将不能正常访问段内数据,这是本篇文章得出的最重要的结论。
最后我们来解释剩下的问题,为什么有时候去掉org指令程序也能正常执行?实际上不加org就相当于没有指示出程序加载的段内偏移值,这时编译器会默认用0做为偏移(相当于org 0x0的情况)。这样的程序只要加载到段内偏移0x0处都能正常执行,还有一种情况就是程序中没有对内存的寻址操作,那么也不会出错,因为org指令就是调整段内地址引用值的。