汇编语言学习笔记(二)

0x00 源程序

程序周期

一个汇编语言程序从写出到最终执行的过程包括如下内容:

  • 编写汇编程序

    这一步工作的结果产生存储源程序的文本文件,常以.asm结尾。

  • 对源程序进行编译连接

    使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件(以.obj结尾)。再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件(以.exe结尾)。这一步工作的结果产生可在操作系统中运行的可执行文件。

    可执行文件包括程序数据相关描述信息三部分。

  • 执行可执行文件中的程序。

  • 程序返回

以上过程可概括如下:

伪指令

汇编语言源程序中的指令包括汇编指令和伪指令。汇编指令有对应的机器码指令,可被编译为机器指令,最终被CPU执行。而伪指令没有对应的机器码指令,由编译器执行,编译器根据伪指令进行相关的编译工作。

  • segment&ends:成对使用,功能为定义一个段。其中segment说明段的开始,ends说明段的结束。
  • end:是一个汇编程序的结束标记,编译器碰到伪指令end则结束对源程序的编译。
  • assume:将有特定用途的段和相关的段寄存器关联起来。

源程序与程序

将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行、处理的指令或数据成为程序。程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。

程序加载&返回

一个程序结束后,将CPU的控制权交还给使它得以运行的程序,这个过程称为程序返回。可以通过在程序的末尾添加如下返回程序段实现程序返回。

1
2
mov ax,4c00h
int 21h

任何通用的操作系统,都需要提供一个称为shell的程序,用户使用这个程序来操作计算机系统进行工作。DOS系统中有一个程序command.com,这个程序称为命令解释器,也就是DOS系统的shell。

故可执行文件在DOS中加载执行的过程可概括如下:

  • 可执行文件执行时,正在运行的command程序将该可执行文件中的程序加载入内存。
  • command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行。
  • 程序运行结束后,返回到command中,CPU继续运行command。

1

由上图可知,程序加载后,ds中存放着程序所在内存区的段地址,对应的偏移地址为0.则程序所在的内存区地址为ds:0。这个内存区的前256个字节中存放的是PSP,DOS用来和程序进行通信。从256字节处向后的空间存放程序。故程序的物理地址为SA+10H:0

语法错误和逻辑错误

一般来说,程序在编译时被编译器发现的错误是语法错误。源程序经过编译,在运行时发生的错误是逻辑错误。

0x01 编辑&编译&连接

DOS下汇编源程序的编辑、编译及连接需要的工具分别为编辑器edit、编译器masm和连接器link。通常来说,DOSBox中不自带这些软件,下载链接如下:https://pan.baidu.com/s/1BxI4qu-3wjPmNOB5ADYFxw ,提取码:yxg5 。

编辑

在DOS命令行中输入edit 文件名.asm指令,进行汇编源程序编辑,界面如下:

2

完成编辑后保存即可。

编译

在DOS命令行中输入masm指令进入编译模式。编译过程中,输入为源程序文件,最多可以得到三个输出即目标文件、列表文件和交叉引用文件。

3

  • Source filename:源程序名称,默认为[.ASM]。若待编译文件为masm所在路径下的.asm文件,则直接输入文件名;否则需要输入完整的路径名称及文件后缀。
  • Object filename:生成OBJ文件名称,默认为[.OBJ]。直接键入Enter则在当前目录下生成.obj文件。当然也可以指定目录。
  • Source listing:列表文件名称,为编译过程的中间结果,键入Enter键取消生成。
  • Cross-reference:交叉引用文件名称,键入Enter键取消生成。

常见编译错误包含两类:

  • 程序中存在Severe Errors
  • 找不到所给出的源程序文件。

连接

连接的作用包括以下内容:

  • 分割较大的源程序,分别编译子源程序后连接在一起,形成一个可执行文件。
  • 将库文件和目标文件连接在一起,实现对库文件中子程序的调用。
  • 连接程序将目标文件中的内容处理为最终的可执行信息。

在DOS中输入link指令进入连接模式。

4

  • Object Modules:待连接OBJ文件名称,默认为[.OBJ]。如果文件不以.obj为扩展名,则需要输入全名。
  • Run File:生成的可执行文件名称。
  • List File:映像文件的名称,为可忽略的中间结果。
  • Libraries:库文件名称。若程序中调用了某一个库文件中的子程序,则在连接时需要将这个库文件和目标文件连接到一起生成可执行文件。

简化版的编译连接

  • 简化编译:masm 文件路径+文件名;,自动忽略中间文件。
  • 简化连接:link 文件路径+文件名;,自动忽略中间文件。

0x02 [BX]&LOOP

定义描述性符号(),其中的元素包括三种类型:寄存器名、段寄存器名和内存单元的物理地址(一个20位数据)。

约定Idata表示常量。

[BX]

[bx]表示一个内存单元,偏移地址在bx寄存器中,段地址在ds寄存器中。

LOOP

使用loop指令实现循环功能,寄存器cx中存放循环次数。该指令的格式是:loop 标号,CPU执行loop指令时,首先执行(cx) = (cx)-1,随后判断cx中的值,若不为零则跳转至标号处执行程序;若为零则向下执行。

使用cx和LOOP指令配合实现循环功能的框架如下:

1
2
3
4
	mov cx, 循环次数
s:
循环执行的程序段
loop s

编写汇编程序并使用debug命令进行跟踪执行,验证loop语句的操作流程。

可以使用debug命令的g命令进行持续执行。如g 0012表示从当前的CS:IP指向的指令执行,一直到(IP)=0012h为止。若希望将循环一次执行完,可在遇到loop指令时,使用p命令执行。

值得注意的是,在汇编源程序中,数据不能以字母开头。

Debug与masm对指令的不同处理

在debug中的指令mov ax,[0]表示将ds:0处的数据送入ax中。但在汇编源程序中,指令mov ax,[0]被编译器当作指令mov ax,0进行处理。Debug将[idata]解释为一个内存单元,idata是内存单元的偏移地址;而编译器将[idata]解释为idata。

若希望在源程序中实现将内存单元中的数据送入寄存器中,则可以使用bx寄存器存储偏移地址,使用[bx]的方式访问内存单元。若希望直接使用idata表示偏移地址,则需要显式地给出段地址所在的段寄存器,例如ds:[idata]

出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的ds:cs:ss:es:,在汇编语言中称为段前缀

安全段空间

在8086模式中,随意向一段内存空间写入内容是危险的,因为这段空间中可能存放着重要的系统数据或代码。故向内存空间写入数据时,需要使用操作系统分配的内存空间,而不应直接用地址任意指定内存单元,向里面写入。

DOS和其他合法的程序一般都不会使用0:200~0:2ff的256个字节的地址空间,故可以直接向该段空间内写入内容。

0x03 多程序段程序

前面内容中讨论的程序均只包含一个代码段,称为单程序段程序,下面讨论多程序段程序。

操作系统环境中,合法地通过操作系统取得的空间都是安全地,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间冲突。故在操作系统允许的情况下,程序可以取得任意容量的空间

程序可以通过两种方法获取所需空间,本文重点讨论在加载程序的时候为程序分配这种方法,即通过在源程序中定义段来获取内存空间。

在代码段中使用数据

考虑以下场景,我们需实现多个常数的相加,并将结果存储在特定寄存器中。这种情况下,我们可以在程序中定义希望处理的数据,这些数据就会被编译、连接程序作为程序的一部分写入可执行文件中。以下面的程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

mov bx,0
mov ax,0
mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21h
code ends
end

dw&db

dw:即define word,用于定义字型数据。该语句后跟随N个字型数据,其所占空间大小为2N字节。同时,dw定义的数据处于代码段的头部,故偏移地址为0。

db:即define byte,用于定义字节型数据。该语句后跟随N个字节型数据,其所占空间大小为N字节。

start

5

通过debug查看上述程序的可执行文件,可以发现代码段的前16个字节是用dw指令定义的数据,从第16个字节开始才是汇编指令对应的机器码。故我们需要显式地表明程序第一条指令的位置

可以通过调试器修改CS:IP指向的指令单元从而标记程序的第一条指令,但更方便的做法是利用start标号来表明程序首条指令的位置。这个标号在伪指令end后面出现,于是end除通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。修改后的程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

start: mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21h
code ends
end start

在代码段中使用数据的程序框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:code
code segment
:
:
数据
:
:
start:
:
代码
:
code ends
end start

在代码段中使用栈

考虑以下场景,我们需要逆序输出数据段中的内容,最佳的解决办法就是在代码段中引入栈。即在程序中通过定义数据来取得一段空间,然后将这段空间当作栈来使用。以下面程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
; 用dw定义16个字型数据,在程序加载后,将取得16个字的内存空间,存放这16个数
; 据。在后面的程序中将这段空间当作栈来使用。
start: mov ax,cs
mov ss,ax
mov sp,30h

mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s

mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0

mov ax,4c00h
int 21h

codesg ends
end start

CS:10~CS:2F的内存空间当作栈使用,初始状态下栈为空,所以SS:SP要指向栈底CS:30

将数据、代码、栈放入不同的段

为避免程序结构混乱,考虑将数据、代码和栈放入不同的段,并在程序头部进行寄存器关联。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h

mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s

mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0

mov ax,4c00h
int 21h
code ends
end start
  • 定义一个段的方法和定义代码段方法相同,只是不同的段要求不同的段名。

  • CPU如何处理定义的段中的内容完全依靠程序中具体的汇编指令,和汇编指令对CS:IPSS:SPDS等寄存器的设置来决定。以上三个寄存器分别掌管代码段、栈段和数据段。

  • 对于如下定义的段:

    1
    2
    3
    name segment
    ...
    name ends

    如果段中的数据占N个字节,则程序加载后,该段实际占有的空间为(N/16+1)*16个字节。

请作者吃个小鱼饼干吧