前言
没想到我有一天会去研究汇编,这是我从未设想的道路。😭
Debug命令
概述
Debug是DOS、Windows都提供的实模式(8086 方式)程序的调试工具。使用它,可以查看CPU各种寄存器中的内容,内存的情况和机器码级跟踪程序的运行。
功能
- R命令 查看、改变CPU寄存器的内容。
- D命令 查看内存中的内容。
- E命令 改写内存中的内容。
- U命令 将内存中的机器指令翻译成汇编指令。
- T命令 执行一条机器命令。
- A命令 以汇编指令的格式在内存中写入一条机器指令。
基础指令与寄存器
寄存器层级
在 x86 架构的汇编语言中,寄存器的命名和使用方式有一定的规律。在Debug中,使用R
指令可以看到寄存器中的内容
![[Pasted image 20250308200824.png]]
以AX为例,他的内容是0000
,他拥有16位
,在汇编指令中AX
即直接代表这个16位
寄存器,如果是AH
和AL
则代表8位
寄存器,H
是高位,L
是低位,当前是X86的,如果是64的他还有32位
,即EAX
。
MOV指令
MOV是做替换,在DEBUG中,执行下面汇编指令
mov ah,13
mov bl,33
mov ch,al
执行过后ax寄存器的高位变成了13,bx的低位变成了33,cx的低位变没变,因为他把cx的高位修改成了ax的低位,ax的低位本身没内容即00。
![[Pasted image 20250308201648.png]]
ADD指令
ADD是做加法,在DEBUG中,执行下面汇编指令
add ax,13
add bx,8
add cx,bx
AX从1300变成了1313,因为加了13,bx从0033变成了003B,他这里展示的是16进制,3+8=11,11刚好对应B,然后CX本身就是0000,加上bx那就是003B
![[Pasted image 20250308203125.png]]
SUB指令
SUB是做减法,在DEBUG中,执行下面汇编指令
sub ax,8
mov bx,F
sub ax,bx
第一条是1313-8,即130B(11),之后bx改为F(15),用ax即130B-F则AX=12FC,相当于130B(11)-15 12F(15)C(12)
![[Pasted image 20250308204550.png]]
MUL指令
mul是乘法指令,使用mul做乘法的时候需要注意下面两点
- 相乘:两个相乘的数,要么都是8位,要么就都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中,如果是16位,一个默认在ax中,另一个放在16位reg或内存字单元中。
结果:如果是8位乘法,结果默认放在AX中,如果是16位乘法,结果高位默认在DX中存放,低位在AX中放。
例如我要做100*10的运算,他应该是执行下面的汇编代码mov al,64 mov bl,A mul bl
64是100的16进制,A是10的16进制,然后mul是运算,得到03E8,03E8刚好是1000的16进制。
![[Pasted image 20250308211341.png]]
下面做一个100*10000的运算,他会不会出现溢出?100小于255,可是10000大于255,所以必须走16位的乘法,汇编代码如下mov ax,64 mov bx,2710 mul bx
这个正常的答案应该是1000000,16进制即F4240,在执行之后的结果中,因为位数不够他会把低位放到AX中,高位放到DX中,DX即F,AX就是4240,实际就是F4240即1000000。
![[Pasted image 20250308212718.png]]DIV指令
div是除法指令,使用div做除法的时候应该注意一下问题
- 除数:有8位和16位两种,在一个reg或内存单元中。
- 被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果出书为16位,被出示则为32位,在DX和AX中存放,DX存放高位16位,AX存放低位16位。
结果:如果除数为8位,则AL存储触发操作的商,AH存储触发操作的余数;如果储是为16位,则AX存储除法操作的商,DX存储法操作的余数。
下面做一个10000/100的一个计算,10000的16进制是2710,100的16进制是64,那么计算的汇编代码将是这样mov ax,2710 mov bl,64 div bl
运行div之后他把答案最终放置到了ax中,64即16进制的100,10000/100=100
![[Pasted image 20250309114424.png]]
上面这种情况一个是被除数是16位,如果计算1000000/10000,10000000的16进制数是F4240,很显然16位数是放不下的,那就需要把高位放到dx中,参考下面的汇编代码mov dx,F mov ax,4240 mov bx,2710 div bx
成功运行那道ax中的运行结果64即10进制的100。
![[Pasted image 20250309115246.png]]
如果运算结果中有余数呢?这里也可以做一下试验,1000001/10000的结果应该是100余1,我们执行下面汇编代码来查看结果mov dx,F mov ax,4241 mov bx,2710 div bx
这里发现余数会存储在dx寄存器中。
![[Pasted image 20250309115715.png]]
上面的是一个32位占用的被除数,如果被除数是8位,则AL存储除法操作的商,AH放余数,可以参考10001/100,汇编代码如下mov ax,2711 mov bl,64 div bl
结果应该是100余1,对应的al的值则就是64,ah的值则就是1
![[Pasted image 20250309120520.png]]AND指令
and指令的作用是按位进行与运算,举个例子
![[Pasted image 20250309121405.png]]
0和0则就是0,1和0还是0,1和1即1,参考汇编代码mov al,63(01100011B) and al,3B(00111011B)
执行过后会把内容丢到al中,运行结果应该是00100011即16进制的23。
![[Pasted image 20250309121909.png]]OR指令
or指令的作用是按位进行或运算,举个例子
![[Pasted image 20250309122149.png]]
0和0就是0,1和1就是1,1和0就是1,0和1也是1,参考汇编代码mov al,63(01100011B) or al,7B(01111011B)
二进制结果应该是01111011,16进制即7B。
![[Pasted image 20250309122321.png]]SHL和SHR指令
shel和shr分别代表左移和右移,左移就是左边去除一个右边补0,右移则就是右边移除一个左边补零。如下图
![[Pasted image 20250309123232.png]]
参考汇编代码如下mov al,63 shl al,1 shr al,1
63的二进制是01100011即16进制63,左移一位二进制变成11000110即16进制C6,右移一位则就变回去了即01100011即16进制的63。
![[Pasted image 20250309123522.png]]
ROL和ROR指令
他俩是循环左移和循环右移,他和普通的左移和右移区别是,循环左移是把最右边的一位补到最左边,循环右移是把最左边的一位补到右边,参考下面的汇编代码查看区别
mov al,FF
rol al,1
shl al,1
FF的2进制是11111111,不管是循环左移还是右翼都是会把一边的1补到另一边,而shl和shr是补0。
![[Pasted image 20250309130428.png]]
INC和DEC指令
INC和DEC的作用是增加1和减少1,例如C语言中的++和--,例子汇编代码如下
mov al,1
inc al
dec al
这个不错介绍,需要注意的是,如果本身是al是00,再去给他dec(--)那么他就会变成FF,对应的如果是al是FF再去给他inc(++)那就会变成00。
![[Pasted image 20250309131638.png]]
NOP指令
这个指令是空代码段,执行不会干任何事情,他所占的空间恰好是1个字节。这里不多说,后面会用到。
XCHG指令
xchg指令的作用是做数据调换,参考下面汇编代码
mov al,11
mov bl,22
xchg al,bl
al和bl的内容互换了,如果是不通过这个指令来去作交换,则需要三个地方存储数据,A把数据给C,然后B把数据给A,再然后C给B这样才能呼唤,这个指令可以直接交换。
![[Pasted image 20250309132351.png]]
NEG指令
neg的作用是取反并加 1,如果原数据为00000001
,通过neg取反+1就是11111110
+00000001
,具体汇编代码参考如下
mov al,1
neg al
00000001取反就是11111110,再去加1那就是FF了,这里不多说。
![[Pasted image 20250309140106.png]]
INT指令
INT指令的含义是中断,在做除法运算的时候如果除数为0那就会自动触发int 0
这个指令,也可以手动执行这个指令,这个指令最终会把内存的执行指针做一个跳转,跳转到最初的位置,具体参考案例如下,第一个是除法除以0,汇编代码如下
mov ax,123
mov bl,0
div bl
运行到div命令开始执行除法的时候,把CS和IP调到F000和1060,这个就是执行int 0会发生的事,出现问题则会跳转到这个位置。
![[Pasted image 20250309142035.png]]
第二个例子直接运行int 0
int 0
![[Pasted image 20250309142243.png]]
这俩寄存器是用来存储执行代码位置的,他这里直接跳转了。
中断编号有很多,这里是调用的0,除法出现错误也会调用中断0,还有很多后面慢慢接触就可以了。
进阶指令与寄存器
物理地址、段地址、偏移地址关系
我这里说的都是基于8086CPU的内容,其他的可能和我这个不一样的。CPU在访问内存的时候,会用一个基础地址(段地址*16)和一个相对地址的偏移地址相加,给出内存单元的物理地址。
更一般的说,8086CPU的这种寻址功能是“基础地址+偏移地址=物理地址”寻址模式的一种具体实现方案。8086CPU中,段地址x16可看作是基础地址。
段寄存器
在8086CPU中,访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址。这里,需要看一下,什么是部件提供段地址。段地址在8086CPU中段寄存器中存放。8086CPU有4个段寄存器:CS、DS、SS、ES。当8086CPU要访问内存时由这四个段寄存器提供内存单元的段地址。
关于内存写入数据
通过Debug程序的e命令可以直接对内存中的数据做修改,可以参考下面截图
![[Pasted image 20250309183659.png]]
DS寄存器-数据段地址
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段和地址。举个例子,通过下面debug命令在21f6:0000
的位置写入一点内容,命令如下
![[Pasted image 20250310085436.png]]
再去修改DS寄存器的内容,DS寄存器的内容应该是数据段的地址,例如采用debug命令去直接修改DS(段寄存器是不可以直接mov数值修改,需要通过其他寄存器进行赋值)寄存器的内容,参考命令截图如下
![[Pasted image 20250310085535.png]]
通过mov去赋值DS
mov ax,21f6
mov ds,ax
要注意直接去mov ds,21f6
是不可行的,他是一个段寄存器在设计的时候就不允许这样。
去执行下面的汇编代码
mov al,[0]
执行之后结果如下
![[Pasted image 20250310085639.png]]
al寄存器变成了12,这个12是哪里来的呢?在执行mov指令的时候,给的值是[0]
这个值是指基于数据段地址的偏移,也就是基于DS,21F6这个位置的第0偏移的数据内容给al,即12。
在看一个案例,还是上面的内容,执行下面的汇编指令
mov bx,[2]
结果如下
![[Pasted image 20250310093528.png]]
为什么BX是7856?bx给的是216f的第2位,也就是从56开始,数值应该是5678
,变成7856的原因是因为他要对其高低位,低位在bl,高位在bh,高位是78,低位是56,对其之后bx就是7856。
CS和IP指令
CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器,从名称上我们可以看出它们和指令的关系。先看个案例,我们先在2000:0000的位置写入一些汇编指令,参考指令如下
a 2000:0000
mov ax,0123
mov bx,0003
mov ax,bx
add ax,bx
![[Pasted image 20250310095922.png]]
执行过后看一下对应位置的内容,发现里面的内容根本看不懂,这里的内容是刚才汇编指令的机器码,可以使用debug的u命令去看这些内容到底是执行的什么内容,参考下图
![[Pasted image 20250310100052.png]]
使用u命令可以看到咱们刚才输入的指令,具体怎么执行这些指令呢,这里就可以通过修改cs和ip寄存器来指定咱们写入命令的位置,再去使用t即可执行咱们这些指令,具体操作如下图
![[Pasted image 20250310100419.png]]
这个时候就相当于命令的指针指向了这里,通过t命令去执行命令结果如下
![[Pasted image 20250310100546.png]]
发现咱们再2000:0000设置的指令都依次执行了。
JMP指令
jmp是一个跳转指令,具体的作用可以做一个实践,根据下图把命令写入内存。
![[Pasted image 20250310101904.png]]
具体命令参考如下
![[Pasted image 20250310102246.png]]
我们把指针跳转到2000:0000开始执行命令,具体命令参考如下
![[Pasted image 20250310102728.png]]
当我们执行第二次的时候也就是命令jmp 1000:3
的时候,cs和ip变成了1000和0003,下一个命令就会去执行mov ax,0000
了,继续执行查看结果
![[Pasted image 20250310103523.png]]
继续执行会发现,他后面会有个jmp指令,jmp的参数是bx,bx是0000,就是把ip改为0000,那就是从mov ax,0123
从头继续执行,然后一直重复,如果一直去执行那么这就是一个死循环。
栈概念
栈是一种后进先出的数据结构,通常用于存储临时数据、管理函数调用和返回地址。栈在内存中通常从高地址向低地址增长,用于保存寄存器值、局部变量等。简单说,就是程序运行时的“临时记事本”。
SS和SP寄存器
这两个寄存器用来定义栈顶的位置,基于栈的操作都是基于这俩寄存器指定的位置来做操作。这俩也是段指令,无法直接mov xx,数值
来直接赋值,得通过mov 寄存器,段寄存器
来修改,或者通过debug名的r命令去修改。
PUSH和POP指令
push就是压栈,指的是将一个元素添加到栈的顶部。pop就是出栈,指的是从栈的顶部移除并返回一个元素。
push和pop指令具体可以参考下图
![[Pasted image 20250310112303.png]]
下面做个实验,我们先指定1000:0010这块内存作为栈实验的栈顶,用来做压栈和出栈的实验,通过debug的r命令来修改ss和sp段寄存器,如下图
![[Pasted image 20250310112811.png]]
我们根据最上面的指令进行操作,我们先把指令写进去然后一步一步去执行,具体的汇编指令如下
mov ax,0123
push ax
mov bx,2266
push bx
mov cx,1122
push cx
pop ax
pop bx
pop cx
先去执行前两条查看一下栈里的内容有啥变化
![[Pasted image 20250310113246.png]]
0123被压入栈了,而且是顶部,并且SP也发生了更变,我们继续把压栈的命令都执行完,还剩4条,看一下结果
![[Pasted image 20250310113424.png]]
全部压进去了,并且位置是往小的来推进的。
接着我们继续执行指令,还有三条pop指令,先执行一条查看一下效果
![[Pasted image 20250310113748.png]]
原本栈内的2211,已经被丢到ax中了,并且他是把第一个丢到低位,然后第二个丢到高位中,这个操作不回去平衡高低位。接着继续执行两个pop命令,执行结果如下
![[Pasted image 20250310114159.png]]
栈内的数据以此丢到了bx和cx中了,并且栈内也没内容了,这里的内容其实我举得例子不好,应该找一块全空的位置来去做这个实验。这里他自动填充了其他数据。
BX寄存器的独特性
bx寄存器有一个额外的作用,就是它可以来指明内存单元,这个是其他大部分寄存器做不到的,举个例子,在2000:100的位置放一些数据,命令如下
![[Pasted image 20250310141934.png]]
修改DS数据段的地址为2000,然后通过偏移位置来去设置其他寄存器的内容,命令如下
![[Pasted image 20250310142305.png]]
这里发现ax成功更变成了2000:100位置的内容,这个在上面也进行过,下面开始通过bx来去当作编译来去拿数据,命令如下
mov bx,0102
mov ax,[bx]
![[Pasted image 20250310142447.png]]
这里发现通过bx的偏移设置了ax的内容,他还有其他的写法,具体命令如下
mov ax,[bx-1]
![[Pasted image 20250310142632.png]]
这样也是可以的,本身bx是0102
通过[bx-1]
那就是0101
的偏移位置来写入ax,把寄存器当作偏移的操作只有bx能做到,尝试其他的会报错,具体错误可以参考下图
![[Pasted image 20250310142844.png]]
使用mov cx,[ax]
和mov ax,[cx]
都是不可行的,这个功能是bx单独的功能但不是他独有的功能,后面会说其他的,这个bx寄存器也一般用来存储偏移地址。
SI和DI寄存器
si和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用。具体使用方法和上面BX寄存器一样。然后这些可以用来当作偏移来用的寄存器可以相加,例如下面汇编指令
mov cx,[bx+si]
mov cx,[bx+di]
# 下面这个不允许
mov cx,[si+di]
BP寄存器
他和上面的BX、SI、DI用处都是可以指明内存单元。但是BP在用法上和它们三个是有区别的,BX、SI、DI采用这三个寄存器去寻址的时候,他是基于DS寄存器来做偏移找内容,而BP是基于SS寄存器,这个SS寄存器上面也讲述了他是用来设置栈顶的段地址的,BP也可以理解为基于栈顶的位置偏移找内容。具体可以参考下面的案例,我在5000:0000
和6000:0000
的位置存放了一些内容,一个正序一个倒叙
![[Pasted image 20250310154216.png]]
下面我把这俩位置分给数据段地址和栈顶的位置,参考下面代码
![[Pasted image 20250310154440.png]]
之后开始测试数据,具体执行的汇编代码如下
mov bx,0001
mov ax,[bx]
mov bp,0001
mov ax,[bp]
![[Pasted image 20250310154637.png]]
ax现在的内容是5000:0000
位置偏移为1的内容,我们再去看bp的内容,继续执行两次查看结果
![[Pasted image 20250310154752.png]]
ax变成了A9CB,这里的内容是从6000:0000
中拿出的。要注意的是bp是可以配合其他的寄存器和偏移使用的,但是他不可以配合bx寄存器来用,因为bx是基于DS寄存器的。具体参考下面汇编代码,以及报错输出
mov ax,[bp]
mov ax,[bp+1]
mov ax,[bp+si]
mov ax,[bp+di]
# bp和bx不能一起用
mov ax,[bp+bx]
错误输出如下
![[Pasted image 20250310155515.png]]
寻址寄存器相关总结
![[Pasted image 20250310155832.png]]
标志位寄存器
CPU内部的寄存器中,有一种特殊的寄存器(低于不通的处理机,个数和结构都可能不同)具有以下三种作用。
- 用来存储相关指令的某些执行结果。
- 用来为CPU执行相关指令提供行为依据。
用来控制CPU的相关工作方式。
这些特殊的寄存器在8086CPU中,被称为标志寄存器。8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。我们已经使用过8086CPU的ax、bx、cx、dx、si、di、bp、sp、ip、cs、ss、ds、es等13个寄存器了,当前章节的标志寄存器(以下简称flag)是我们要学习的最后一个寄存器。
flag和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。
8086CPU的flag寄存器的结构图如下
![[Pasted image 20250310162649.png]]
flag的1、3、5、12、13、14、15位在8086CPU中没有使用,不具有任何含义。而0、2、4、6、7、8、9、10、11位都具有特殊的含义。
在这一章节中,我们学习标志寄存器中的CF、PF、ZF、SF、OF、DF标志位,以及一些与其相关的典型指令。ZF标志
flag的第6位是ZF,0标志位。他记录相关指令执行后,其结果是否为0。如果结果为0,那么zf=1;如果结果部位0,那么zf=0。
参考下面汇编指令,来去验证zf的变动mov ax,1 sub ax,1
执行结果在Debug中的体现如下图,先把ax改成1,执行之后NZ那个东西就是ZF标识即
NoZero
应该是这样理解,这是我自己理解的。然后等sub命令执行之后ax变成了0,ZF标志位变成了ZR,他代表zero
![[Pasted image 20250310165002.png]]PF标志
flag的第二位是PF,奇偶标志位。他记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。如果1的个数为偶数,pf=1,如果奇数,那么pf=0。在命令中的具体体现可以参考下面汇编代码
mov ax,0000 add ax,1 add ax,1 add ax,1
执行结果如下,pe即代表偶数也就是pf=1,po即代表是奇数也就是pf=0
![[Pasted image 20250310170031.png]]
然后这里有个问题,算奇数偶数为什么ax的值是1和2的时候都是奇数呢?因为他算的是一个bit中的1的数量,ax=0001的时候,他的bit8位表示是00000001
他就一个1所以标识奇数,ax=0002的时候,他的bit8位标识是00000010
,还是一个1,所以还是一个奇数。