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

开发 前端
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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

新建文本文件,把代码 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

仍然是将其保存为 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
  • 1.
  • 2.
  • 3.

是设置栈段的栈顶指令,执行完成后会设置栈顶的物理地址为 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

编写完毕,编译链接成 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

然后再重新链接成为 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-06-28 11:01:08

2023-05-19 07:31:48

2024-07-22 09:52:42

2024-07-12 09:21:38

负载均衡HTTP网络

2023-12-11 08:03:01

Java线程线程组

2022-11-18 12:03:01

2024-01-19 08:25:38

死锁Java通信

2024-02-04 00:00:00

Effect数据组件

2023-07-26 13:11:21

ChatGPT平台工具

2023-01-10 08:43:15

定义DDD架构

2024-01-02 12:05:26

Java并发编程

2023-08-01 12:51:18

WebGPT机器学习模型

2019-06-19 08:09:05

CSSJavaScript前端

2023-04-26 07:46:21

2023-02-15 08:41:56

多层维表性能宽表

2025-01-16 00:17:44

2024-01-26 06:05:16

KuberneteseBPF网络

2024-04-29 06:55:34

RustMIDI应用程序

2024-05-06 00:00:00

InnoDBView隔离
点赞
收藏

51CTO技术栈公众号