【原创】自制操作系统知识储备(二)---一定要搞清楚ORG

汇编语言中的ORG问题:只有彻底搞清楚了ORG的原理,你才能在汇编语言和操作系统开发中游刃有余。

ORG的作用不是指定程序的放置位置,它没有那样的功能和能力!这点,对于做过单片机项目的人来说,特别容易上当。如在程序的开头放上一句:ORG 500H,代表的含义并不是指程序是就从内存500H的地方开始放。ORG语句是否需要、ORG语句后面的数值应该是多少,并不是写程序的人主观指定的,而是被动指定的!

(i) 什么情况下不需要ORG?
<1> 不涉及从内存取数据的时候,程序不需要ORG。ORG有无不影响程序指令的顺序执行,也即不影响CS、IP等值。ORG的影响往往只是针对DS、ES等来说的。
<2> 程序没有被强制指定存放位置的时候(默认在段的0偏移地址开始放置程序),程序不需要ORG。
(ii) 什么情况下需要ORG?
当程序被强制指定存放位置的时候,必须要有ORG。
典型的以下两种场景:
<1>DOS下面的.com程序,由于程序被强制指定了只能放在段内PSP之后且在100H的地方,因此程序必须要用ORG。如果不用ORG 100H指令,当访问低端内存地址的时候(如MOV AX,[02H])就一定会错误的访问到PSP里面的数据了。而用了ORG 100H指令之后,以上语句,就能访问到正确的内存地址:100H+02H。
关于这块知识,我认为经典的教程在这个地方:
https://blog.csdn.net/ruyanhai/article/details/7177904
<2>系统引导扇区程序:由于计算机强制指定了只能放在7C00H的内存地址,因此程序必须要用ORG。如果不用ORG 7C00H,那么语句MOV AX,[02H],访问到的内存地址也将是错误的地址02H,而用了ORG 7C00H之后,访问到的内存地址就是正确的地址: 7C00H+02H。
关于这块知识,我认为经典的教程在这个地方:
https://blog.csdn.net/daiyutage/article/details/8918290
(iii) 为什么需要ORG?
我们写汇编程序的时候,程序最终以.COM或.EXE或.BIN等形式放在计算机的内存什么位置是无法预知的。那么在编译阶段,如果程序需要从内存取数据进行操作,也就无从预知实际的物理地址,那么程序是不是就没有办法运行了呢?为了解决这个问题,编译原理只能将程序中的内存位置定义成相对位置,也即任何对内存地址的定位都翻译成离程序最开始的偏移量。这样,无论最终的程序从内存什么地方开始放,都不会影响程序的正确运行:
在这里插入图片描述
可见,程序无论放在什么位置,只需要将相对偏移量+程序放置位置,就能得到准确的物理位置。而这个累加计算工作,就是计算机在将程序装载到内存的时候,产生二进制BIN格式过程中要完成的工作,最终得以保证程序正确运行。我们知道相对偏移量在编译阶段由编译器产生,那么程序放置位置由谁产生呢?无疑,就只有ORG。
(iv) ORG应该怎么取值?
显然,程序从哪个地方开始放置,就应该取多少值。因此,对.COM来说,程序是从100H的地方开始,ORG就只能取100H。引导扇区是从7C00H的地方开始,ORG也就应该取7C00H。只有这样取值,计算机才能在最终的程序中准确定位变量在内存中的物理位置。
(v) 如何避免使用ORG?
实际上,你可以看到并不是所有的汇编程序都一定需要ORG。比如引导扇区程序,如果我们自定义了DS和ES的值,就不再需要在程序的开头加上ORG 7C00H。但是这个DS和ES的值必须要有严格的值,使得程序相对DS和ES来说,刚好是从0偏移位置开始放置才行。
MOV AX, 7C0H
MOV DS, AX
MOV ES, AX
显然以上语句的意思是,由于程序开始放置的物理地址是7C00H,刚好可以看成是段地址7C0,加上0偏移地址。也即:7C0H10H+0000H。这个时候程序开头就不能再用ORG了,对于语句MOV AX, [02H]。将要访问的物理地址就是DS:02H(7C0H10H+02H),这就是正确的物理地址。如果没有指定DS的值(引导扇区刚启动的时候,默认DS=00H,ES=00H),以上语句就只能写成:MOV AX, [7C00+02H]才能达到同样的目的。当然,你也可以在程序的开头设置ORG 7C00H,语句MOV AX, [02H]就是正确的,这就又回到了前面的章节内容。
(vi) ORG详细测试
为了把ORG的问题彻底搞清楚,我们通过测试来仔细研究:

(a)无ORG的测试
在这里插入图片描述
该程序的目标是打印出welcome字符串,我们先编译成.COM:

在这里插入图片描述
运行结果,与我们的预期并不一致,我们只需要示“‘Welcome Jiang OS’”字符串,但是程序却打印出了额外的很多信息。为什么呢?我们来看看机器语言,打开org.lst:
在这里插入图片描述
可以看到,mov si, welcome 这语句取到的地址是0002。
再打开最终运行的二进制文件org.bin,看看:

在这里插入图片描述
可以看到,程序运行的时候最终数据读取的内存偏移地址是:0002,这与我们的初衷是不相符合的,因为.COM程序是强制安装在段内100H偏移处的,而我们要取的数据相对偏移又是0002H,因此最终的偏移地址应该是100H+0002H=0102H才对。所以,取到的数据就不正确。那么我们从0002H处误取到的数据都是些什么呢?就是PSP,也即DOS系统的.COM程序长度为256B的前缀!

在这里插入图片描述

(b) 有ORG的测试

现在我们加上ORG再进行对比测试:

在这里插入图片描述
编译后运行结果如下:

在这里插入图片描述
可以看到,这个就得到了我们想要的结果。我们同样来看看编译过程中的机器语言文件:org.lst

在这里插入图片描述

可以看到,汇编器编译成机器语言之后,取数据这条指令的
相对地址仍然是0002H。这就是我所说的,编译阶段,编译器能做的工作就只能得出相对地址,这个地址显然无法取得正确的数据。因此,最后的实际地址还必须看二进制文件,我们这次又来打开最终的二进制文件来看看:

在这里插入图片描述
很明显,这次取到的地址就不再是0002H,而是0102H!这就是我们真正需要取的正确的数据地址:这个地址也正确的跳过了.COM程序的PSP部分,避免造成错误。
通过2次调试过程,我们很明显的看出:在汇编成机器语言的时候,程序只是全部汇编出相对偏移地址。在生成2进制文件程序进驻内存的时候,编译器必须要“悄悄”的把ORG的地址和相对偏移地址相加,才能得到最终的数据访问地址。所以,我们要看最终的地址是否正确,一定要看最终的BIN二进制文件!

(c)在必须要用ORG的情况下,如何做到不用?

通过以上的过程,我们可以看到,ORG是必须的。那么如果我们非要“固执”的不用ORG,到底行不行呢?答案是可以的。通过前面的分析,我们可以看出,要让最终的实际地址和相对偏移地址相等,唯一的办法就是要让程序加载在段内偏移地址为0的地方,也即要让程序从段地址开始。要达到这个目的,我们可以通过手工设置段寄存的方法实现。
还是以上面的.COM程序为例,由于程序强制的段内偏移地址是:
100H,那物理地址是:CS*10H+100H=(CS+10H)*10H。也即:将现有DS\ES寄存器的地址修改为:CS+10H,那么程序内凡是使用DS和ES访问的地址都是从段内偏移地址为0的地方开始计算的,即可理解为程序是从段内偏移地址为0的地方开始加载的(其实程序加载的物理地址并没有变化)。修改程序如下:
在这里插入图片描述
将程序编译并运行之后,结果果然如期:

在这里插入图片描述

那我们再反过来思考一下,如此设置DS和ES之后,最终的二进制文件中的偏移地址是多少呢?应该是0002H,而不能再是1002H。因为现在的地址都应理解成从段内偏移地址为0的地方开始加载,打开最终的二进制文件验证如下:

在这里插入图片描述
终极结论

通过以上的分析,我们可以得出普遍的规律就是,要想不使用ORG,只需根据CS和程序偏移地址计算并重置DS,ES即可:
DS,ES=(CS*10H+ORG)/10H=CS+ORG/10H
因此,无论程序从什么地方开始放置,只要用以下通用语句,就可以完全不用ORG语句。
mov ax, cs
add ax, org/10h
mov ds, ax
mov es, ax

(vii) ORG 7C00H 详细测试
我们再用上面的结论和方法验证一遍,计算机中最常用到的启动扇区ORG 7C00H的案例。

(a) 无ORG 7C00H测试
在这里插入图片描述

运行结果,必然是打印乱码,因为取内存数据welcome找错了地方:

在这里插入图片描述
显然从BIN来看,由于没有ORG 7C00H,本来应该取
7C00H+0002H=7C02H的内存,结果取到了00002的内存,也就是中断向量区数据。

在这里插入图片描述

(b) 有ORG 7C00H测试

在这里插入图片描述

运行结果,必然是正确的,因为这次指定了ORG 7C00H。

仔细的人可能看出来了,在ORG 7C00H中我们对DS进行了初始化成0的操作,而在ORG 100H中,我们没有对DS做初始化操作。对这个问题,说明如下:
ORG 100H 是DOS系统分配一段内存给.COM程序运行,也就是说我们的.COM程序始终是在DOS当前段里面的,我们无需重新设置CS、DS、ES等寄存器,直接使用这些寄存器就能正常访问内存,因为我们所有的操作都是在这个段内进行的,如果重置这些寄存器的值,反而还会出错。
而ORG 7C00H则是不同,程序运行起来的时候,计算机没有操作系统,在到ORG 7C00H时,开始运行第一条指令,所有的寄存器CS、DS、ES值都为0,只有IP寄存器非0为7C00H(这段话我是从某些教科书上看到的,但后来我用反证法证明这个说法是错误的。因为计算机在到达7C00H之前已经运行了BOIS相关程序了,所以这些寄存器的值并不一定为空)。这样的情况下,CS、DS、ES这些值我们就可以自己设置,但这里由于我们已经指定了ORG 7C00H,那么DS,ES的值则只能置0,才能保证我们在程序中用DS和ES访问到的地址和二进制中ORG地址+相对偏移地址保持一致。如本例中,要访问的实际地址是7C00H+0002H=7C02H。
但是,这里始终有一个令我一直疑惑的问题。既然开机启动的时候,所有的寄存器CS、DS、ES值都为0(关于这点我通过看各种资料,都是这么说的)那为什么还需要在程序中认为置一次0呢?也如下3句话:
mov ax, cs
;或mov ax, 0
mov ds, ax
因为,我在程序中去掉这3行之后,引导系统启动后,程序无法正常显示我需要的字符串,显然是取内存中的数据出现了问题。

在这里插入图片描述

问题是,我通过查看二进制的最终程序文件,发现写进内存中的程序需取的内存地址并没有问题,是正确的,我想要的7C02H!

在这里插入图片描述
继续深挖,既然取地址没有问题,而取出的数据打印出来出现了问题,那么只有2种可能,第1,内存中的数据有问题,也即在7C02的地方不是我们想要的welcome。但这种情况,我们通过看二进制的内容一下就能排除,通过下图可以看到数据是正确的字符串。

在这里插入图片描述
第2,就是寄存器地址DS有问题。我们取数据地址的计算公式为:DS*10H+7C02H(这个值已经以BIN格式写进了内存,不会错),而实际的物理地址=7C02H,要让这两个值相等,只有1个选择,那就是DS=0。现在出了问题,只有1种可能,那就是DS不等于0。如果说计算机启动引导扇区之后,DS初始值不等于0,那它究竟是多少呢?由于没有调试工具,无法得知。因此,后面出于无奈,我只有在程序中手工将DS置0,这样才能得到正确的结果,这在前面的调试过程中已经写清楚了。暂时,我对这个问题原因的推测只能是:计算机在将启动程序装载到7C00H处后,执行第1条指令前,CS=0,IP=7C00H。但是DS\ES肯定不是0,因为在这之前CPU在硬件自检等环节还需要读BIOS高端内存地址的大量数据(计算机启动时,CS=FFFF,IP=0, 系统是由地址0xFFFF0开始执行的),DS\ES的值早已经修改很多遍了。
其实,不管也不用纠结CS、DS和ES是否为0,因为我们用下面的通用语句就能达到正常访问数据的目的:
Mov ax, cs
Mov ds,ax
Mov es,ax
因为这三行程序的目的是让代码和数据都同一个64K段内任意访问。

(c) 完全不用 ORG 7C00H的测试

再回到上一节的重要结论,当时说了,对于任何程序,只需要如下公式就可以不用ORG:
DS,ES=(CS*10H+ORG)/10H=CS+ORG/10H
我们用这个通用公式,在ORG 7C00H上做一次验证:

在这里插入图片描述

程序果然能正常的运行:

在这里插入图片描述

可以推测,由于没有使用ORG 7C00H且重新设定了段DS和ES的值,因此二进制程序的最终地址是0002H。验证如下:

在这里插入图片描述

(结束语)汇编开发方法
用汇编语言编写操作系统,对于很多人来说挑战极大,原因在于汇编语言编程不像高级语言那样容易实现逻辑模块划分,所以我们看到的大部分汇编语言程序都是上来就是一大段一大段令人恐惧的代码,读者阅读并领会别人甚至自己写的一段程序非常的吃力。这是我观察所有的汇编语言程序之后得出的重要结论!
因此,我的汇编语言程序风格必须改变这种现状,需要达到一个目标:那就是我写的程序,放在若干年之后,我都能非常快的阅读入手。我写的程序,让别的读者看了之后也能最快的时间内入手。
要达到这个目标,必须且唯一采用的方法就是:任何程序都必须把它模块化。因为,越是低级的语言,越是需要模块化包装!坚决屏弃掉那种一上来就直接是一大段代码的写作风格。为此,我找到了一套非常好的写作方法:功能逐步剥离、程序模块包装。

  1. 功能逐步剥离
    在完成任何任务前都先分析步骤和功能,然后再把每个任务继续剥离成子任务,这样一步一步的推进,直到所有任务完成。
  2. 程序模块包装
    完成子任务分解之后,每一个子任务就单独包装成程序模块,
    所有的任务完成都采用模块调用的方式进行组织。
    例如,要完成操作系统程序这个开发总任务,我们只需要按如下方法进行组织:
    <1> 总任务:操作系统程序
    任务1:MBR启动
    任务2:LOADER启动
    任务3:APPLICATION启动
    <2> 子任务分解
    (a)分解任务1:MBR启动
    子任务1-1: 程序中加载7C00H,设置正确的DS\ES地址。
    子任务1-2:打印正确欢迎信息,表明MBR成功运行。
    子任务1-3:制作启动引导标记“55AA”。
    子任务1-4:把启动引导扇区的程序大小设置成512B。
    (b)分解任务2:LOADER启动
    子任务2-1:加载LOADER到内存
    (i) 子任务2-1-1: 读扇区
    (ii) 子任务2-1-1: 读磁头
    (iii) 子任务2-1-1: 读柱面
    子任务2-2:在内存执行LOADER
    (i) 子任务2-2-1:跳转到 LOADER存放地址
    (ii) 子任务2-2-2:执行LOADER代码
  3. 树形模式
    按照这样的方式组织的程序就会非常的清晰和明白,对于
    开发和阅读来说,大大的降低了汇编语言的难度。任务的完成就好比我们通过种下一颗颗幼小的种子,最终却长成一颗了参天大树。
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/csdn1340802/article/details/86649490