来看三段程序,你学会了什么?

开发 前端
debug 对我们来说非常重要,有很多代码细节和问题通过肉眼是观察出来的,我们肉眼可能能够判断一些简单的程序问题,但是对于很多隐藏较深的问题,还是要依据 debug 才能发现。

学习任何一门语言都不能少的了 debug ,汇编也是。

debug 程序执行过程

下面我们就依据这几个功能来跟踪一下程序的执行过程。

debug 对我们来说非常重要,有很多代码细节和问题通过肉眼是观察出来的,我们肉眼可能能够判断一些简单的程序问题,但是对于很多隐藏较深的问题,还是要依据 debug 才能发现。

下面是一段汇编代码,这段汇编代码我之前的文章中也给大家写过。

assume cs:codesg
codesg segment

mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax

mov ax,4c00h
int 21h

codesg ends
end

新建文本文件,把代码 cv 过去,然后右键保存,使用 dosbox 将其编译为 1.obj 文件,链接为 1.exe 文件后,我们使用 ​​debug 1.exe​​ 命令来分析一下这段程序,并用 -r 命令来看一下初始的寄存器情况。

图片

程序初始状态下,可以看到 CX 中的数据为 000F,这也表示着程序的长度是 000F,1.exe 中共有 15 个字节,CX 中的内容为 000FH。

好,现在我们已经知道程序被成功的载入内存并运行起来了,但是我们现在先不妨想一下,被链接成为 EXE 的程序会被装入内存的哪个地方的呢?我们怎么知道程序被装入在哪里呢?

程序装载的过程分下面几步:

  1. 首先程序会从内存中找到一块区域,记为初始地址 SA,此时的偏移地址为 0 的这样一块足够容量的内存区域。

图片

  1. 在这段区域内的头 256 个字节中,会创建一块称为程序段前缀(Program Segment Prefix ,PSP)的区域,这块区域被 DOS 用来和被加载的程序进行通信。

图片

  1. 从这块程序的 256 个字节开始处,也就是在 PSP 程序段前缀的后面,程序会被加载到这里,此时程序的初始地址是 SA + 10H,偏移地址为 0 。也就是 SA + 10H : 0,所以程序的初始地址就是 CS = 076AH ,IP = 0000H。

图片

程序被装入内存后,由 DS 段寄存器存放着内存区的段地址,此时内存区域的偏移量为 0 ,所以此时的物理地址为 SA * 16:0,我们并不用知道真实的 DS 是多少,反正都是由操作系统和 DOS 分配的。

然后这个内存区域的前 256 个字节被用于存放 PSP ,所以程序的物理地址为 SA * 16 + 256 : 0 。

SA * 16 + 256 = SA * 16 + 16 * 16 = (SA  + 16) * 16 ,转换为 16 进制就是 SA + 10H,所以物理地址就是 SA + 10H : 0。

我们上面 debug 1.exe 之后可以看到,DS 段寄存器的值为 076AH ,而 CS 段寄存器的值为 076BH ,正好符合 076A * 16 + 10 = 076BH (注意这里的 * 16 就是左移 4 位的意思,之前文章中也解释过原因。)

我们使用 -u 指令可以看到完整的汇编源代码。

图片

上图中用红框圈出来的就是我们这段汇编程序的源代码,可以看到这是一个程序段,程序段的段地址始终为 076A,偏移地址在不断变化。

我们使用 -t 命令来单步执行以下这段程序,如下图所示。

图片

(为了连续的观察一下程序的执行结果,我索性直接把主要的程序步骤执行完了。)

这段程序就是 mov 和 add 的基本使用,将 0123 送入 AX 寄存器,将 0456 送入 BX 寄存器,对 AX 寄存器执行 AX = AX + BX ,再对 AX 执行 AX = AX + AX。

程序继续向下执行,当执行到 int 21H 处,程序执行完毕,此时要使用 -p 命令结束程序的执行,如下图所示。

图片

当显示 Program terminated normally 时,表示程序正常结束,这里大家先不用考虑为什么执行到 int 21 处才执行 -p 命令,也不用关心 mov ax,4c00 和 int 21 是什么意思,大家先记住就行。

由于程序装载的过程是 command 将程序装载进入内存,然后 debug 程序对 exe 程序其进行跟踪,所以程序退出后也是先从 exe 程序退出到 debug 程序中,由 debug 程序再退回到 command 程序中。

下面再分析一段程序,汇编原代码

assume cs:codesg

codesg segment

mov ax,2000H
mov ss,ax
mov sp,0
add sp,10
pop ax
pop bx
push ax
push bx
pop ax
pop bx

mov ax,4c00H
int 21H

codesg ends

end

仍然是将其保存为 test.txt,然后执行编译和链接操作,将其生成可执行文件 test.exe,观察其执行过程。

我们先使用 -r 查看一下初始寄存器的内容。

图片

主要观察一下 CX 、DS 、CS 和 IP 的值,是否和我们上面描述的一致,CX 存放程序长度,DS 存放程序段地址,CS 存放程序初始地址,IP 存放程序偏移地址。

再使用 -u 看一下 exe 程序的源代码,这个 exe 程序是经过编译和链接之后的程序。

图片

我们来分析一下这段,这是一段栈段的入栈和出栈的程序,首先

mov ax,2000H
mov ss,ax
mov sp,0

是设置栈段的栈顶指令,执行完成后会设置栈顶的物理地址为 20000 H ,即 SS:SP = 2000:0000。

图片

我们执行这个程序的过程中,发现 mov sp,0 这个指令为什么没有出现呢?难道是我们漏写了?查看了一下,源代码确实是有这条指令的,难道是没有执行?

为了验证这个假设,我们重新 debug 一下这段程序,然后先把 SP 的值进行修改,如下图所示。

图片

刚开始,我们使用 -r 把 sp 的值改成 0002,然后单步执行,在执行到 mov ss,ax 之后,发现 SP 的值变为 0000,这也就是说 mov sp,0 这条指令其实是执行了的,只是 debug 模式下没有显示而已。

程序继续向下执行,下面是两个 pop 出栈操作。

图片

pop ax 和 pop bx 做了两件事:把寄存器清空;栈顶位置 + 2 ,所以 ax 和 bx 寄存器的内容为 0 ,并且 SP = SP + 2 ,执行后 SP = 000E。

之后是两个 push 操作,把出栈的两个寄存器再进行入栈,如下图所示。

图片

push 操作也做了两件事情,将寄存器入栈,SP = SP - 2,由于 ax 和 bx 已经 pop 出栈了,所以寄存器内容为 0 ,最后再进行 pop 操作,然后再结束程序的执行过程。

图片

我们再来看一下 PSP 的情况,由于程序被装入的时候前 256 个字节是 PSP 所占用的,此时 DS(SA)处就是 PSP 的起始地址,而 CS = SA + 10H ,也就是 CS = 076AH。

debug 循环程序

下面我们来 debug 一下循环程序,看看有哪些有意思的细节。

现在有这样一道问题,计算 ffff:0006 单元中的数乘 3 ,让结果存储在 dx 中。

针对这个问题,有几个点需要思考:

  • 我们知道 ,8086 汇编语言中单个存储单元所能存储的最大值是 8 位,一个字节长度,范围是 0 - 255 之间,而一个寄存器 dx 中可容纳的最大值是 16 位,两个字节长度,范围是 0 - 65535,即使 255 * 3 也小于 65535,很显然乘以 3 之后,dx 中能够存放的下。
  • 数乘 3 相当于是循环做 add 自身操作 3 次,所以需要用加法来实现乘法,可以直接使用 dx 进行累加,不过需要一个 ax 来进行中转。
  • ffff:6 内存单元是一个字节单元,而 ax 寄存器能容纳的是一个字单元,无法直接赋值,该如何做呢?因为 ax 可以看做 al 和 ah ,而 al 和 ah 又是两个单独的寄存器,它们之间不会发生值溢出,所以让 ah = 0 ,al = 内存单元的值即可。

所以这段汇编程序的代码如下

assume cs:codesg

codesg segment

mov ax,0ffffh
mov ds,ax

mov ah,0
mov al,[6]

mov cx,3
s: add dx,ax
loop s

mov ax,4c00h
int 21h

codesg ends
end

编写完毕,编译链接成 exe 程序后,对其进行 debug xxx.exe 操作。

我们来看下程序的执行过程。

图片

前两段没毛病,设置 DS 段寄存器的值为 FFFF 。然后继续向下执行

图片

执行到 mov al,[6] 的时候我发现,怎么 AX 寄存器中的内容变成 0006 了?我不是想要把 06 放入 ax 中啊,我是想把 ffff:06 内存单元中的值放入 ax 中啊,我突然意识到编译器是个傻子。

经过我认真仔细细心耐心用心的排查了一番问题之后,我方才大悟,原来我是个傻子!不知道各位小伙伴们看出来我代码的问题了吗?

我怎么敢在源程序中把立即数当做内存偏移地址来用呢?必须要用 bx 中转啊!

这也就是说,编译器编译完源代码之后,会把 06 当做立即数使用,如果想要使 06 表示内存地址,必须要用 bx 进行中转,修改之后的源代码如下:

assume cs:codesg

codesg segment

mov ax,0ffffh
mov ds,ax
mov bx,6

mov ah,0
mov al,[bx] # 必须要用 bx 进行中转,才能表示内存地址
mov dx,0 # 累加寄存器清 0

mov cx,3
s: add dx,ax
loop s

mov ax,4c00h
int 21h

codesg ends
end

然后再重新链接成为 exe 程序之后,我们一步一步 debug 看一下。

图片

执行到 mov al,[bx] 的时候,我们发现,此时右侧有个 ds:0006 = 31,这段代码表示的是 ds:0006 处内存单元的值是 31,这才表明我们的程序是正确的。

继续向下执行程序。

图片

前两条指令执行完成后,(dx) = 0 ,(cx) = 3,完成对累加寄存器的清空和循环计数器的赋值操作。最后一条指令是第一次循环操作指令,此时 CS:IP 指向 076A:0012 ,继续向下执行。

图片

可以看到,第一次 add dx,ax 执行完成后 IP = 0014H ,此时指向的指令是 LOOP 0012,这条指令的意思是让程序再执行一次 (IP) = 0012H 处的指令,也就是再执行一次 add dx,ax,可以看到 cx 的值变成了 0002,因为循环指令执行后 (cx) = (cx) - 2 ,然后再向下执行,发现后面的循环指令还是 LOOP 0012 ,再执行一次 add dx,ax,一直到 (cx) = 0 后结束程序执行,如下图所示

图片

可以发现,整个程序一共循环三次,最终 dx 中的值是 93 ,程序执行到 int 21H 处,使用 -p 命令结束程序的执行。

责任编辑:武晓燕 来源: 程序员cxuan
相关推荐

2023-07-26 13:14:13

业务项目技术

2023-05-19 07:31:48

2023-06-28 11:01:08

2023-12-11 08:03:01

Java线程线程组

2024-02-04 00:00:00

Effect数据组件

2024-01-19 08:25:38

死锁Java通信

2023-07-26 13:11:21

ChatGPT平台工具

2023-01-10 08:43:15

定义DDD架构

2022-11-18 12:03:01

2019-06-19 08:09:05

CSSJavaScript前端

2022-07-08 09:27:48

CSSIFC模型

2023-01-30 09:01:54

图表指南图形化

2023-10-10 11:04:11

Rust难点内存

2023-12-12 08:02:10

2022-03-08 08:39:22

gRPC协议云原生

2024-01-02 12:05:26

Java并发编程

2023-08-01 12:51:18

WebGPT机器学习模型

2023-04-26 07:46:21

2023-02-15 08:41:56

多层维表性能宽表

2022-08-30 09:57:28

项目传统对象访问
点赞
收藏

51CTO技术栈公众号