在这里插入图片描述

前言

📌

汇编语言是很多相关课程(如数据结构、操作系统、微机原理)的重要基础。但仅仅从课程的角度出发就太片面了,其实学习汇编语言可以深入理解计算机底层工作原理,提升代码效率,尤其在嵌入式系统和性能优化方面有重要作用。此外,它在逆向工程和安全领域不可或缺,帮助分析软件运行机制并增强漏洞修复能力。

本专栏的汇编语言学习章节主要是依据王爽老师的《汇编语言》来写的,和书中一样为了使学习的过程容易展开,我们采用以8086CPU为中央处理器的PC机来进行学习。

1. 引言

前面的程序中,只有一个代码段。现在有一个问题是,如果程序需要用其他空间来存放数据,使用哪里呢?前面的内容中,我们讲到要使用一段安全的空间。可哪里安全呢?

我们说 0:200~0:2FF是相对安全的,可这段空间的容量只有256个字节,如果我们需要的空间超过 256个字节该怎么办呢?

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

程序取得所需空间的方法有两种:

  • 一是在加载程序的时候为程序分配

  • 二就是程序在执行的过程中向系统申请。

在我们的内容中,不讨论第二种方法。

加载程序的时候为程序分配空间,我们在前面已经有所体验,比如我们的程序在加载的时候,取得了代码段中的代码的存储空间。

我们若要一个程序在被加载的时候取得所需的空间,则必须要在源程序中做出说明。我们通过在源程序中定义段来进行内存空间的获取

上面是从内存空间获取的角度上,谈定义段的问题。我们再从程序规划的角度来谈一下定义段的问题。大多数有用的程序,都要处理数据,使用栈空间,当然也都必须有指令,为了程序设计上的清晰和方便,我们一般也都定义不同的段来存放它们。

对于使用多个段的问题,我们先简单说到这里,后面我们将以这样的顺序来深入地讨论多个段的问题:

(1)在一个段中存放数据、代码、栈,我们先来体会一下不使用多个段时的情况。

(2)将数据、代码、栈放入不同的段中。

本文先来看第一点。

2. 在代码段中使用数据

2.1 问题引入

考虑这样一个问题,编程计算以下8个数据的和,结果存在ax寄存器中:

0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h

在前面的文章中,我们都是累加某些内存单元中的数据,并不关心数据本身。可现在要累加的就是已经给定了数值的数据。我们可以将它们一个一个地加到ax寄存器中,但是,我们希望可以用循环的方法来进行累加,所以在累加前,要将这些数据存储在一组地址连续的内存单元中。

如何将这些数据存储在一组地址连续的内存单元中呢?我们可以用指令一个一个地将它们送入地址连续的内存单元中,可是这样又有一个问题,到哪里去找这段内存空间呢?

从规范的角度来讲,我们是不能自己随便决定哪段空间可以使用的,应该让系统来为我们分配。我们可以在程序中,定义我们希望处理的数据,这些数据就会被编译、连接程序作为程序的一部分写到可执行文件中。

当可执行文件中的程序被加载入内存时,这些数据也同时被加载入内存中。与此同时,我们要处理的数据也就自然而然地获得了存储空间。

2.2 代码实现

具体的做法看下面的程序。

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 2lh

code ends

end

2.3 代码分析

2.3.1 提出问题

解释一下,程序第一行中的“dw”的含义是定义字型数据。dw 即“define word”。在这里,使用 dw 定义了8个字型数据(数据之间以逗号分隔),它们所占的内存空间的大小为16个字节。

(1)程序中的指令就要对这8个数据进行累加,可这8个数据在哪里呢?

由于它们在代码段中,程序在运行的时候CS中存放代码段的段地址,所以可以从CS中得到它们的段地址

(2)它们的偏移地址是多少呢?

因为用dw定义的数据处于代码段的最开始,所以偏移地址为0,这8个数据就在代码段的偏移0、2、4、6、8、A、C、E处。程序运行时,它们的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。

(3)

程序中,用bx存放加2递增的偏移地址,用循环来进行累加。在循环开始前,设置(bx)=0,cs:bx 指向第一个数据所在的字单元。每次循环中(bx)=(bx)+2,cs:bx 指向下一个数据所在的字单元。

2.3.2 编译、链接、加载、查看程序

将上面的程序编译、连接为可执行文件p61.exe,先不要运行,用Debug 加载查看下,情况如下图所示。

在这里插入图片描述

上图中,通过“DS=0B2D”,可知道程序从0B3D:0000开始存放。用u命令从0B3D:0000查看程序,却看到了一些让人读不懂的指令。

但是,为什么没有看到程序中的指令呢?

实际上用u命令从0B3D:0000查看到的也是程序中的内容,只不过不是源程序中的汇编指令所对应的机器码,而是源程序中,在汇编指令前面,用dw定义的数据。实际上,在我们的程序中,有一个代码段,在代码段中,前面的16个字节是用“dw”定义的数据,从第16个字节开始才是汇编指令所对应的机器码。

2.3.3 查看内存内容

可以用d命令更清楚地查看一下程序中前16个字节的内容,如下图所示。

在这里插入图片描述

2.3.4 正确的执行程序

可以从0B3D:0010查看程序中要执行的机器指令,如下图所示。

在这里插入图片描述

从上面两个图中,我们可以看到程序加载到内存中后,所占内存空间的前16个单元存放的是源程序中用“dw”定义的数据,后面的单元存放源程序中汇编指令所对应的机器指令

怎样执行程序中的指令呢?

用Debug加载后,可以将IP设置为10h,从而使CS:IP指向程序中的第一条指令。然后再用t命令、p命令,或者是g命令执行。

2.4 面临的问题

可是这样一来,我们就必须用Debug来执行程序。之前的程序编译、连接成可执行文件后,在系统中直接运行可能会出现问题,因为程序的入口处不是我们所希望执行的指令

2.5 改进代码

如何让这个程序在编译、连接后可以在系统中直接运行呢?

我们可以在源程序中指明程序的入口所在,具体做法如下。

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

2.5.1 程序的执行过程

注意在上面的程序中加入的新内容,在程序的第一条指令的前面加上了一个标号start,而这个标号在伪指令end的后面出现。这里,我们要再次探讨end的作用。end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在上面的程序中我们用end指令指明了程序的入口在标号 start 处,也就是说,“mov bx,0”是程序的第一条指令。

根据前面的内容,我们已经知道在单任务系统中,可执行文件中的程序执行过程如下。

(1)由其他的程序(Debug、command或其他程序)将可执行文件中的程序加载入
内存。

(2)设置CS:IP指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行。

(3)程序运行结束后,返回到加载者。

2.5.2 CPU如何知道第一条执行什么指令?

现在的问题是,根据什么设置CPU的CS:IP指向程序的第一条要执行的指令?也就是说,如何知道哪一条指令是程序的第一条要执行的指令?

这一点,是由可执行文件中的描述信息指明的。我们知道可执行文件由描述信息和程序组成程序来自于源程序中的汇编指令和定义的数据描述信息则主要是编译、连接程序对源程序中相关伪指令进行处理所得到的信息

我们在刚才的程序中,用伪指令end描述了程序的结束和程序的入口。在编译、连接后,由“end start”指明的程序入口,被转化为一个入口地址,存储在可执行文件的描述信息中。

在刚才程序生成的可执行文件中,这个入口地址的偏移地址部分为:10H。当程序被加载入内存之后,加载者从程序的可执行文件的描述信息中读到程序的入口地址,设置CS:IP。这样CPU就从我们希望的地址处开始执行。

归根结底,我们若要CPU从何处开始执行程序,只要在源程序中用“end 标号”指明就可以了。

2.6 程序的框架

有了这种方法,就可以这样来安排程序的框架:

assume cs:code

code segment
		;
		;
		数据
		;
		;
start:
		;
		;
		代码
		;
		;
code ends

end start

3. 在代码段中使用栈

3.1 问题引入

完成下面的程序,利用,将程序中定义的数据逆序存放。

assume cs:codesg

codesq segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
	?
codesg ends

end

程序的思路大致如下。

程序运行时,定义的数据存放在cs:0~cs:F单元中,共8个字单元依次将这8个字单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。

问题是,我们首先要有一段可当作栈的内存空间。

如前所述,这段空间应该由系统来分配。可以在程序中通过定义数据来取得一段空间,然后将这段空间当作空间来用

3.2 代码实现

程序实现如下。

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		;将设置栈顶ss:sp指向cs:30

		mov bx,0
		mov cx,8
	s:	push cs:[bx]
		add bx,2
		loop s			;以上将代码段0~15单元中的8个字型数据依次入栈

        mov bx,0
		mov cx,8
	s0: pop cs:[bx]
		add bx,2
		loop s0			;以上依次出栈8个字型数据到代码段0~15单元中

		mov ax,4c00h
		int 21h
codesg ends

end start				;指明程序的入口在 start 处

3.3 重要说明

(1)

注意上面程序中的指令:

mov ax,cs
mov ss,aX
mov sp,30h

我们要将cs:10~cs:2F的内存空间当作栈来用,初始状态下栈为空,所以ss:sp要指向栈底,则设置 ss:sp指向 cs:30。如果对这点还有疑惑,建议回头认真复习一下之前栈的相关内容。

(2)

在代码段中定义了16个字型数据,它们的数值都是0。这16个字型数据的值是多少,对程序来说没有意义。

我们用dw定义16个数据,即在程序中写入了16个字型数据,而程序在加载后,将用32个字节的内存空间来存放它们。这段内存空间是我们所需要的,程序将它用作栈空间。可见,我们定义这些数据的最终目的是,通过它们取得一定容量的内存空间。

所以我们在描述dw的作用时,可以说用它定义数据,也可以说用它开辟内存空间。比如对于:

dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

可以说,定义了8个字型数据,也可以说,开辟了8个字的内存空间,这段空间中每个字单元中的数据依次是:0123h、0456h、0789h、0abch、0deth、0fedh、0cbah、0987h。因为它们最终的效果是一样的。

结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下。

也可以点点关注,避免以后找不到我哦!

Crossoads主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的动力!

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐