二进制安全-汇编语言-04-第一个程序
四、第一个程序
随心而动
本章节主要学习如何把前面学习的汇编代码,用编译和链接程序将它们编译链接成可执行文件。
4.1 一个源程序从写出到执行的过程
(1)编写汇编源程序
(2)对源程序进行编译链接
-
汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件obj
-
再用链接程序对目标文件进行链接,生成可在操作系统直接运行的可执行文件exe
-
可执行文件包含两部分内容:
- 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
- 相关的描述信息(比如,程序有多大、要占用多少内存空间等)
(3)执行可执行文件中的程序
操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,
并进行相关的初始化(比如设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。
4.2 源程序
assume cs:codesg codesg segment ;代码段开始mov aX,0123Hmov bx,0456Hadd aax,bxadd ax,axmov ax,4c00HINT 21H
codesg ends ;代码段结束
end
1、伪指令
在汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。
汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。
而伪指令没有对应的机器指令,最终不被CPU所执行。
那么谁来执行伪指令呢?
伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。
(1)xxx segment
xxx ends
segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。
segment和ends的功能是定义一个段,segment说明一个段开始,ends说明一个段结束。
一个段必须有一个名称来标识,使用格式为:
段名 segment..
段名 ends如上述示例
codesg segment ;定义一个段,段的名称为“codesg”,这个段从此开始..
codesg ends ;名称为“codesg”的段到此结束
(2)end
end是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束对源程序的编译。
所以,在我们写程序的时候,如果程序写完了,要在结尾处加上伪指令end。
否则,编译器在编译程序时,无法知道程序在何处结束。
注意:不要搞混了end 和 ends
ends 和 segment 成对使用,标记一个段的结束,ends可以理解为” end segment “的缩写
end是标记整个程序的结束
(3)assume
这条伪指令的含义为“假设”。它假设某一段寄存器和程序中的某一个用segment…ends定义的段相关联。
通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。
assume并不是一条非要深入理解不可的伪指令,以后我们编程时,记着用assume将有特定用途的段和相关的段寄存器关联起来即可。
比如,在程序4.1中,我们用codesg segment…codesg ends定义了一个名为codseg的段,
在这个段中存放代码,所以这个段是一个代码段。
在程序的开头,用assume cs:codesg
将用作代码段的段codesg和CPU中的段寄存器cs联系起来。
2、源程序中的“程序”
源程序:源程序中的所有内容(这里有伪指令)
程序:将源程序中最终由计算机执行、处理的指令或数据
程序最先是以汇编指令的形式存在源程序中,经编译、链接后转变为机器码,存储在可执行文件中
3、标号
汇编源程序中,除了汇编指令和伪指令外,还有一些标号,比如“codesg”。
一个标号指代了一个地址。
比如codesg在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
4、程序的结构
assume cs:abcabc segmentmov ax,2add ax,axadd ax,ax
abc endsend1、我们要定义一个段,名称为abc
2、在这个段中写入汇编指令,来实现我们的任务。
3、然后,要指出程序在何处结束。
4、abc被当作代码段来用,所以,应该将abc和cs联系起来。
(当然,对于这个程序,也不是非这样做不可。)
5、程序返回
下面,我们在DOS(一个单任务操作系统)的基础上,简单地讨论一下这个问题。
一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得以运行。P2开始运行后,P1暂停运行。
而当P2运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P1,此后,P1继续运行。
现在,我们知道,一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回。
那么,如何返回呢?应该在程序的末尾添加返回的程序段。
mov ax,4c00H
int 21H
这两条指令所实现的功能就是程序返回,这里不多解释,学到后面中断就能理解了
现在记住,能用就行
书中总结的与结束相关的概念:
目的 | 相关指令 | 指令性质 | 指令执行者 |
---|---|---|---|
通知编译器一个段结束 | 段名 ends | 伪指令 | 编译时,由编译器执行 |
通知编译器程序结束 | end | 伪指令 | 编译时,由编译器执行 |
程序返回 | mov ax,4c00H int 21H | 汇编指令 | 执行时,由CPU执行 |
6、语法错误和逻辑错误
语法错误,编译器会报错
逻辑错误,需要自己去发现
4.3 编译源程序
可以使用windows自带的notepad(记事本)编写好程序,这里还推荐notepad++和Subline Text
(1)进入DOS方式,运行Edit,
(2)在Eidt中编辑程序
(3)将程序保存为文件1.asm后,退出Edit,结束对源程序的编辑
4.4 编译
不多说,放图演示
我们可以进入masm文件夹下,看看有没有生成的目标文件1.obj,验证一下是否编译成功,一般是没问题的
我们用WinHex这个软件查看一下1.obj里面有什么:
除了1.asm,其他是乱码,也可能是我不知道怎么看吧,先到这了
4.5 链接
链接步骤直接看图
链接成功,这里1.exe
这里大致说一下,我对编译和链接的浅显认识:
首先,我们编写好汇编语言代码asm
编译:将我们写的汇编语言转成机器可识别的机器语言,即0和1
这里,我们会生成目标文件obj
链接:将我们程序所需要的文件(这里包括目标文件obj,或者库文件,或者其他目标文件等),串联到一起,
程序执行时需要什么代码,它就自己拿什么代码,类似造汽车,需要什么零件,程序就拿什么零件
最后,我们要封装成一个能跑的汽车,即可执行文件exe
这里是书本的官方说法
好了,我们简单地讲连接的作用,连接的作用有以下几个。
(1)当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件;
(2)程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件;
(3)一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。
4.6 以简化的方式进行编译和链接
编译
masm c:\1链接
link 1
这里简单做了个测试,是可以的(这里为了测试,改的test文件名)
4.7 1.exe的执行
如图执行就好了,这个简单程序执行了,但是没有返回结果,故等待后续学习了,做点有意思的事情
4.8 谁将可执行文件中的程序装载进入内存并使它运行?
我们在前面讲过,在DOS中,可执行文件中的程序P1若要运行,必须有一个正在运行的程序P2,将P1从可执行文件中加载入内存,将CPU的控制权交给它,P1才能得以运行;当P1运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P2。按照上面的原理,再来看一下4.7节中1.exe的执行过程(思考相关的问题)。
(1)在提示符“c:masm”后面输入可执行文件的名字“1”,按Enter键。这时,请思考问题4.1。
(2) 1.exe中的程序运行。
(3)运行结束,返回,再次显示提示符“c:lmasm”。请思考问题4.2。
问题4.1
此时,有一个正在运行的程序将1.exe中的程序加载入内存,这个正在运行的程序是什么?它将程序加载入内存后,如何使程序得以运行?
问题4.2
程序运行结束后,返回到哪里?
我们先来聊一聊操作系统的外壳(shell)
这里先说一下windows中的cmd吧,win键+R键,在输入框输出cmd,就会出现一个黑框
我们就会进入一个系统文件夹,C:\Users\koko 操作系统就会在这等待你输入命令
比如我们常见的dir命令,那么我们就可以查看到当前路径下的文件信息了
在这么一个场景基础下,我们再来理解DOS
DOS中有一个程序command.com,这个程序在DOS中称为命令解释器,也就是DOS系统的shell
DOS启动时,先完成其他重要的初始化工作,然后运行command.com,command.com 运行后,执行完其他的相关任务后,
在屏幕上显示出由当前盘符和当前路径组成的提示符,比如:“c:\”或“c:lwindows”等,然后等待用户的输入。
用户可以输入所要执行的命令,比如,cd、dir、type等,这些命令由command执行,command执行完这些命令后,再次显示由当前盘符和当前路径组成的提示符,等待用户的输入.
如果用户要执行一个程序,则输入该程序的可执行文件的名称,command首先根据文件名找到可执行文件,然后将这个可执行文件中的程序加载入内存,设置CS:1IP指向程序的入口。此后,command暂停运行,CPU运行程序。程序运行结束后,返回到command中,command再次显示由当前盘符和当前路径组成的提示符,等待用户的输入.
在DOS中,command处理各种输入:命令或要执行的程序的文件名。我们就是通过command来进行工作的。
回顾到上面的问题上总结一下,
dos中的command程序一直在运行,等待用户输入命令,
用户输入了命令,它就把CPU交出来,让输入的命令来执行,
命令结束后,CPU有回到command那里,实现交互式
官方回答
现在回答问题4.1和4.2中所提出的问题。
(1)在DOS中直接执行1.exe时,是正在运行的 command,将1.exe中的程序加载入内存;
(2)command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行;
(3)程序运行结束后,返回到command中,CPU继续运行command。
4.9 程序执行过程的跟踪
下面以在前面的内容中生成的可执行文件1.exe为例,讲解如何用Debug对程序的执行过程进行跟踪。
现在我们知道,在DOS中运行一个程序的时候,是由command将程序从可执行文件中加载入内存,并使其得以执行。但是,这样我们不能逐条指令地看到程序的执行过程,因为command的程序加载,设置CS:IP指向程序的入口的操作是连续完成的,而当CS:IP一指向程序的入口,command就放弃了CPU的控制权,CPU立即开始运行程序,直至程序结束。
为了观察程序的运行过程,可以使用Debug。Debug可以将程序加载入内存,设置CS:IP指向程序的入口,但Debug并不放弃对CPU的控制,这样,我们就可以使用Debug的相关命令来单步执行程序,查看每一条指令的执行结果。
(1)在提示符后输入debug 1.exe
,按enter键,Debug将程序从1.exe中加载入内存,进行相关的初始化后设置CS:IP指向程序的入口
(2)用R命令查看各个寄存器的设置情况
可以看到,Debug将程序从可执行文件加载入内存后,cx中存放的是程序的长度。
1.exe中程序的机器码共有15个字节。则 1.exe加载后,cx中的内容为000FH。
现在程序已从1.exe中装入内存,接下来查看一下它的内容,可是我们查看哪里的内容呢?
程序被装入内存的什么地方?我们如何得知?
重点
这里,需要讲解一下在DOS系统中.EXE文件中的程序的加载过程。
简单理解一下:
程序要执行,那么就需要一块空闲的内存
(1)起始地址为SA:0000(即起始地址的偏移地址为0)
Program Segment Prefix,想具体了解我附加在文章末尾(源于deepseek)
这里简单阐述一下:这块区域用来给操作系统和用户程序进行通信的,可用作程序返回、中断、提供信息等
(2)空闲内存区的前256字节(100H),创建一个程序段前缀(PSP)
(3)程序装入在紧跟PSP后,程序的地址为SA+10H:0
(4)该内存区的段地址存入ds中,初始化其他寄存器后,就设置CS:IP指向程序的入口
小结:
空闲内存区:SA:0
PSP区:SA:0
程序区:SA+10H:0
注意,有一步称为重定位的工作在图4.20中没有讲解,因为这个问题和操作系统的关系较大,我们不作讨论。
那么,我们的程序被装入内存的什么地方?我们如何得知?从图4.20中我们知道以下的信息。
(1)程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为ds:0;
(2)这个内存区的前256个字节中存放的是PSP,DOS用来和程序进行通信。从256字节处向后的空间存放的是程序。
所以,从ds中可以得到PSP的段地址SA,PSP的偏移地址为0,则物理地址为SA×16+0。
因为PSP占256(100H)字节,所以程序的物理地址是:SA×16+0+256 = SA×16+16×16+0=(SA+16)×16+0
可用段地址和偏移地址表示为:SA+10H:0。
现在,我们看一下图4.19中DS的值,DS=129E,则PSP的地址为129E:0,程序的地址为12AE:0(即 129E+10:0)。
图4.19中,CS=12AE,IP=0000,CS:IP指向程序的第一条指令。
注意,源程序中的指令是mov ax,0123H,在Debug中记为mov ax,0123,这是因为Debug默认所有数据都用十六进制表示。
可以用U命令查看一下其他命令
现在,我们可以开始跟踪了,用T命令单步执行程序中的每一条指令,并观察每条指令的执行结果,
到了int21,我们要用P命令执行
int 21执行后,显示出“Program terminated normally”,返回到Debug中。表示程序正常结束。注意,要使用P命令执行int21。这里不必考虑是为什么,只要记住这一点就可以了。
需要注意的是,在DOS中运行程序时,是command将程序加载入内存,所以程序运行结束后返回到command中,而在这里是Debug将程序加载入内存,所以程序运行结束后要返回到Debug中。
使用Q命令退出 Debug,将返回到command中,因为 Debug是由command加载运行的。
在DOS中用“debugl.exe”运行Debug对1.exe进行跟踪时,
程序加载的顺序是:command加载Debug,Debug加载1.exe。
返回的顺序是:从1.exe中的程序返回到Debug,从Debug返回到command。
PSP的拓展
8086 CPU 的 PSP(Program Segment Prefix,程序段前缀) 是 MS-DOS(以及兼容系统如 PC-DOS、DR-DOS)在加载一个可执行程序(.COM 或 .EXE)到内存时,自动在该程序所占内存块的最前端(偏移地址 0000h 处)创建的一个 256 字节(100h 字节)的数据结构。它扮演着操作系统与用户程序之间关键通信桥梁的角色,主要功能包括:
-
提供程序终止返回地址:
- PSP 的开头(偏移 00h)包含一条
INT 20h
指令的机器码(CD 20)。 - 当 .COM 程序执行到其代码段末尾(或通过跳转到 PSP:0000h)时,执行这条指令会触发 DOS 的中断 20h,通知操作系统该程序已经结束,DOS 会回收其占用的内存并将控制权交还给父进程(通常是命令行解释器 COMMAND.COM)。
- .EXE 程序通常使用
INT 21h
的功能 4Ch (MOV AH, 4Ch; INT 21h
) 退出,但 PSP 中的INT 20h
仍然是其内存结构的一部分。
- PSP 的开头(偏移 00h)包含一条
-
存储程序结束地址:
- 偏移 0Ah 处存放一个
INT 22h
中断向量的远地址(先偏移,后段地址)。 INT 22h
是 DOS 的程序结束地址。当程序正常或异常终止时,DOS 会将控制权转移到这里指定的地址。这通常是 COMMAND.COM 中负责清理和准备接收下一条命令的代码。
- 偏移 0Ah 处存放一个
-
存储 Ctrl+C 处理程序地址:
- 偏移 0Eh 处存放一个
INT 23h
中断向量的远地址。 - 当用户在程序运行时按下
Ctrl+C
或Ctrl+Break
,DOS 会调用INT 23h
。PSP 中存储的这个地址指向默认的命令行中断处理程序(通常在 COMMAND.COM 中),它负责终止当前程序。程序可以修改这个地址指向自己的Ctrl+C
处理程序。
- 偏移 0Eh 处存放一个
-
存储严重错误处理程序地址:
- 偏移 12h 处存放一个
INT 24h
中断向量的远地址。 - 当发生严重的硬件错误(如磁盘读写失败)时,DOS 会调用
INT 24h
。PSP 中存储的地址指向默认的严重错误处理程序。程序也可以修改这个地址指向自己的错误处理例程。
- 偏移 12h 处存放一个
-
传递命令行参数:
- 偏移 80h 处的一个字节存储命令行参数的长度(不包括末尾的回车符)。
- 偏移 81h 开始的 127 个字节存储实际的命令行参数字符串,这个字符串以回车符(0Dh)结尾(不是 C 语言风格的 null 结尾)。
- 程序可以通过读取 PSP:80h 和 PSP:81h 来获取用户在启动它时输入的命令行选项和参数。例如,运行
COPY A.TXT B.TXT
,COPY
程序就能在 PSP+81h 处找到A.TXT B.TXT
。
-
传递环境变量块:
- 偏移 2Ch 处存储一个段地址(
SEG
)。 - 这个段地址指向另一个由 DOS 分配的内存块,称为环境块。环境块包含一系列以 null 结尾的字符串,格式为
VARIABLE=value
,最后以两个连续的 null 字节(00h 00h)结束。 - 程序可以通过这个指针访问继承自父进程(通常是 COMMAND.COM)的环境变量,如
PATH
,COMSPEC
,PROMPT
等,这对程序查找文件或确定系统配置至关重要。
- 偏移 2Ch 处存储一个段地址(
-
文件句柄表(仅限早期/FCB 相关):
- 在 PSP 中有一个区域(偏移 18h 开始的 20 字节)最初是为与 CP/M 兼容的文件控制块设计的,在 MS-DOS 的后期版本中,它的作用被更强大的文件句柄机制(通过
INT 21h
功能调用管理)所取代,这部分在 PSP 中逐渐变得不那么重要。
- 在 PSP 中有一个区域(偏移 18h 开始的 20 字节)最初是为与 CP/M 兼容的文件控制块设计的,在 MS-DOS 的后期版本中,它的作用被更强大的文件句柄机制(通过
-
DTA 地址:
- 偏移 80h 处(与命令行参数共享空间)也用作默认的磁盘传输区。当程序使用传统的 FCB 方式进行文件操作时,如果没有通过
INT 21h
功能 1Ah 设置自定义的 DTA,DOS 会默认使用 PSP:80h 开始的 128 字节作为数据缓冲区。现代的句柄式文件操作通常使用用户指定的缓冲区。
- 偏移 80h 处(与命令行参数共享空间)也用作默认的磁盘传输区。当程序使用传统的 FCB 方式进行文件操作时,如果没有通过
总结来说,PSP 的主要用途是:
- 为程序提供标准化的退出机制(
INT 20h
, 存储终止处理程序地址)。 - 为程序提供标准化的中断处理机制入口(Ctrl+C, 严重错误)。
- 向程序传递启动信息(命令行参数)。
- 向程序传递环境信息(环境变量指针)。
- 在 DOS 内核和用户程序之间建立必要的联系,确保程序能正确加载、运行、交互和终止,并将资源归还给系统。
理解 PSP 对于深入理解 MS-DOS 环境下程序的加载、执行、参数传递、环境访问以及终止过程至关重要,尤其是在进行底层汇编语言编程或分析 DOS 系统行为时。
实验3 编程、编译、链接、跟踪
(1)将下面的程序保存为t1.asm文件,将其生成可执行文件t1.exe。
assume cs:codesgcodesg segmentmov ax,2000Hmov ss,axmov sp,0add sp,10pop axpop bxpush axpush bxpop axpop bxmov ax,4c00Hint 21Hcodesg endsend
(2)用Debug跟踪t1.exe的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容。
(3)PSP 的头两个字节是CD 20,用 Debug 加载 t1.exe,查看 PSP 的内容。
注意,一定要做完这个实验才能进行下面的课程。
这里偷个小懒,就不用dosbox的edit了,直接记事本新建t1.asm,将代码复制进去
快速编译链接一下
先完成一下任务(3)吧,查看一下PSP的开头的两个字节,确实是CD 20
对于COM程序,PSP是在CS前的100H字节;对于EXE程序,则是DS:0开始的100H字节。
CD 20是指令int 20H的机器码,COM程序最后执行ret执行会跳转到这里,int 20H的作用是退出程序,返回DOS。
这里找到了个网友的回答,CD 20就是int 20h,用于退出程序、返回DOS
如上图,我们要回顾一下几个寄存器的内容的意义:
CX:程序的大小
DS:段地址SA=075A
CS:SA+10H=076A(10H是PSP所占用)
接下来,我们来一步步看一下程序执行
用Debug跟踪t1.exe的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容。
mov ax,2000h
ax=2000,IP+3=0003
mov ss,ax
mov sp,0
ss=2000,sp=0000,ip+5=0008 设置栈顶位置
add sp,10h
sp+10h=000A,ip+3=000B
pop ax
pop bx
push ax
push bx
pop ax
pop bxmov ax,4c00H
int 21H
这段好像比较无聊,没什么结果
到int 21H,用P命令结束