这本书我是在寒假完成的,但是之后我发现那个时候的远古CPU,16位的CPU和现在的64位CPU有着极大的差别,以至于我看完之后连非常基本的print hello的汇编都看不明白,这当然有一部分原因是我学的没那么踏实,但是也说明了这部分知识迭代速度之快。我也终于发现汇编语言与其他所有计算机语言一样,只有自己动手才能真正掌握,光看书是不会学明白的。
12.16
新开坑了,汇编语言一直是我这个逆向手的短板,因为他不仅枯燥,而且枯燥,所以非常之难学,所以我也是想要通过一整个寒假的时间,来把汇编语言看完。
语言
首先我们要知道,我们直接编写的语言(C语言,python)是无法在机器上运行的,因为机器只认识0和1,根本不认识一些字符,所以我们编写的C代码都会先通过编译等一系列手段(C语言的编译过程我之前已经总结过)才能转换为直接运行在电脑上的机器码。
但是C语言和机器码之间也是有着过度的,这就是汇编语言,是一种更加贴近底层的,直接操纵硬件的代码,这样的低级语言学习,对于学习逆向和pwn都有着莫大的帮助。
寄存器
· 一个典型的CPU由运算器、控制器、寄存器组成
1.运算器进行信息处理
2.寄存器进行信息储存
3.控制器控制各种器件进行操作
4.内部总线连接各种器件,在他们之间进行数据的传送
字在寄存器中的存储
需要注意,在写一条汇编指令或一个寄存器的名称时不区分大小写。
并且一个16位寄存器(例如ax)
由两个寄存器组成:ah(high),与al(low)分别对应了ax寄存器的高八位和低八位
另外,由于一个16位寄存器最多只能存储FFFFH大小的数据,所以一旦有数据的大小超过了这一值寄存器只会截取其后16位
寄存器的汇编指令
add ax,bx
mov ax,bx
这样简单的汇编指令很早就学会了
但有一些点需要注意:
首先,一个16位寄存器ax由al,ah组成,所以我们也可以对8位寄存器al,ax进行操作(需要注意!此时假如说al的数值大于了它可以接受的最大值,那么al就会存储改数值的后8位,该数值的前若干位并不会被保存到ah中,在此时CPU认为他们是毫不相干的两个寄存器。)
所以执行mov ax,bl add al,100H等语句都会报错
物理地址
CPU在访问内存单元时,需要知道内存单元的地址
所有内存单元构成的存储空间是一个一维的线性空间,每个内存单元在其中都有唯一的地址(即物理地址)
8086CPU给出物理地址的方法(16位cpu有点古老了哈哈哈)
用两个16位地址形成一个20位地址:
1.cpu中的相关部件提供两个地址:段地址 偏移地址
2.段地址和偏移地址通过内部总线送入一个称为地址加法器的部件
3.地址加法器把两个16位的地址合成一个20位的地址
4.地址加法器通过内部总线将20位物理地址送入输入输出控制电路
5.输入输出控制电路将20位物理地址送上地址总线
6.20位物理地址被地址总线传送到存储器
地址加法器采用物理地址=段地址*16(<<4)+偏移地址的方法合成物理地址
这样一个操作的本质是:物理地址=基础地址+偏移地址
相当于在C语言中给定一个数组a[]那么我们知道其中的所有元素都可以通过*(a+x)来访问到,其中的a就是基础地址,而下标就是偏移地址
这样做的意义在于:就拿数组的地址来打比方,我要写下a[5]的地址,可能是0x0FF46545之类乱七八糟的,但是有a的存在,我只需要写a+5就是他的地址了,所以cpu进行了类似的操作,只要记录下基础地址,那么偏移地址的表达量很小,有助于提升运算速度和减少内存
段地址
其实对于16位机来说基础地址就是段地址*16
段寄存器
给出段地址的部件就是段寄存器,8086cpu有四个段寄存器:CS,DS,SS,ES
讲一下CS和IP寄存器,这两个寄存器指示了cpu当前要读取指令的地址,CS是代码段寄存器,IP是指令指针寄存器
在上述的古老cpu中cpu读的地址就是cs*16+ip(这两个寄存器每次重启内部的数据都会被重置)
IP的值会自动增加,增加的值就是读入的指令长度
理论上在计算机内部,指令和数据没有任何区别,因为都是一堆二进制数,正是由于CS,IP寄存器的存在,使得cpu知道了cs:ip地址所对应的内容是指令,cpu才可以正常运行
修改cs,ip的值
CS,IP与普通的寄存器不同,无法通过mov,add等指令进行修改
修改这二者的值的语句是:
jmp 段地址:偏移地址
(仅修改ip的值只需要jmp 某一合法寄存器的指令就行)
DS和[address]
有下面这样一个操作:
mov bx,1000H
mov ds,bx
mov al,[0]
这样的操作实际上是直接用内存单元中的数据对寄存器赋值 其中0代表了内存地址的偏移地址
另外,上述的前两条指令似乎有些多余
直接mov ds,1000H不久好了吗?
但是由于这个古老的cpu的硬件原因,他不支持这样做
但是我们要知道DS的值仅仅代表了地址,并不是代表地址中的值,相当于C语言中的指针并没有进行解引用
另外mov指令对于段寄存器都是可行的(双向都可以即:mov ax,ds mov ds,ax)
但是add,sub两个算数运算对段寄存器是不可行的,这是由古老cpu的硬件条件决定的。
栈(涉及到SS SP)
(终于到栈了www)
栈是一种具有特殊的访问方式的存储空间,他的特殊性在于,最后进入这个空间的数据,最先出去(其实栈是一种数据类型,先进后出的操作规则被称为LIFO(last in first out))
最基本的出入站命令是PUSH(入栈) POP(出栈)
远古cpu的栈操作都是以字为单位进行的
栈的寻址与cpu寻找指令类似,由两个寄存器分别存储段地址和偏移地址
在远古cpu中为段寄存器SS和寄存器SP
其中栈顶的段地址放在SS中,偏移地址放在SP中,任意时刻SS:SP指向栈底(栈是由高地址向低地址生长的,所以栈顶地址小)(假如说数据地址为cs:2f 那么SS:SP指向的地址应该是cs:30)
同样的pop和push指令对段寄存器和内存单元也可以进行操作
因为SS为段寄存器,所以对其赋值必须由其他的寄存器来起到桥梁的作用(不能直接赋值)
另外的:我们有时候需要把一个寄存器中的内容清空,最容易想到的是mov ax,0但是这样的操作需要占用3个字节,并且速度并不是很快,所以可以有sub ax,ax或者xor ax,ax之类的操作来清空寄存器是比较好的(我一般看见的清空都是xor)
栈溢出
任何一个栈都是有大小的
如果一个16字节大小的占空间被push进了9个数据,那么这时候就发生了栈溢出,然而CPU对于栈溢出是没有保护的,这样那些数据就会溢出到其他的地方,可能会覆盖掉我们真正有用途的数据,这也是pwn栈溢出题目的原理之一
伪指令
在汇编语言中也有一些代码叫做伪指令,汇编指令是有对应机器码的指令,可以被编译为机器指令,最终被CPU执行。伪指令没有对应的机器指令,最终不被CPU执行。伪指令是由编译器来执行的,编译器根据伪指令来进行相关的编译工作
比如说定义一个叫做abc的段:
abc segement
······
abc ends
程序返回
在DOS(单任务操作系统)系统中,p2程序在可执行文件中,那么必须有一个正在运行的程序p1,将p2从可执行文件中载入内存后,将cpu的控制权交给p2,p2才能得以运行。
所以我们将把cpu的控制全交还给使得它得以运行的程序p1的这个过程叫做程序返回
在汇编中就是在程序的末尾添加:
mov ax,ac00h
int 21h
WARNING
作者用了很大篇幅去讲了dos系统下的各种操作,也提及了.asm文件到.exe文件的过程,即编译+连接
但是包括他之前讲的debug这些工具都是过时且古老的,所以这方面对操作系统的学习就直接跳过了
数据类型的判断
之前我提过一个问题:因为所有的数据在计算机中都以二进制存储,所以说计算机并不知道他们的类型是什么,那么我们是通过什么方法来确定一个数据的类型的呢?
现在这个问题可能可以回答:用数据的长度来做判断,一个数据存储在一个内存单元中,他的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)来指出
新指令:inc
即x++
inc bx 相当于
add bx 1(我估计这样做的原因是因为inc bx所需要的机器码长度较小,所以运行速度更快)
同样的自减是dec
loop指令
当CPU执行到loop时,他会进行两个操作:
1.(cx)=(cx)-1
2.判断(cx)不为0则转至标号处执行程序,如果为0则向下执行
显然loop是为了循环功能,而(cx)则显然是循环次数
首先我们要知道如果我们要把ffff:0-ffff:b中的数据(长度为一个字节)累加到dx中,那么直接add dx,[位置]是不行的,因为dx是一个16位寄存器,而数据的长度为8位,两者的长度不匹配,无法进行加法运算。其次,add dl,[位置]也是不可行的,因为那么多个数据可能存在溢出情况,而溢出的值并不会进入dh(忘了的话看前面寄存器的汇编指令)
所以对于这样的问题我们需要引入另一个寄存器ax
把ah赋值为0,利用al来获取内存单元中的值,并且最后用add dx,ax来实现两个操作对象的长度对齐
这样就实现了累加
但是这样写地址一个一个写过去会非常之麻烦,所以需要loop循环的帮助
于是有了下面的程序
mov ax,offffh
mov ds,ax
mov bx,0
mov dx,0
mov cx,12
s:mov al,[bx]
mov ah,0
add ax,ax
inc bx
loop s
在代码段中使用数据
我们之前已经知道,如果我们随意访问,更改一些内存的值,很可能会导致系统的崩溃,因为有的空间中的数据是操作系统所占用的。所以我们在存储数据,进行操作的时候有必要寻找一段安全的空间
例如0123h这个数据占2字的空间,如果这样的数据比较多,那么寻找一块安全的空间必不可少
于是有了dw的出现 dw即define word
dw定义了字型数据,占用2个字节(这样一看dw还可以用来开辟一段安全的内存空间)
并且dw所定义的数据位于代码段的最开始,偏移地址为0
我们把一个程序编译,连接成为exe程序后,查看字节码,我们会发现,最开始的汇编指令并不是我们所编写的汇编指令,因为在操作系统中直接运行exe可能会出现问题,因为程序的入口不是我们所希望执行的代码,让程序在编译链接后可以直接在系统中运行需要在源程序中指明程序的入口所在
需要一个start
······
end start(在这里end的作用不是编译程序结束,而是指明程序入口)
一个程序的框架:
assume cs:code
code segment
······
数据
······
start
······
代码
·······
code ends
end start
摆了两天,QAQ
更灵活的定位内存地址的方法
首先引入两条新的汇编指令:
and 按位与
or 按位或
没啥好说的
一个有意思的判断大小写的方式
判断其二进制数的第五位(从0开始计算,代表数字32)如果是1,那么就是小写字母,如果是0那么就是大写字母
所以说单方向的要求大写转小写或者小写转大写只需要and或者or一下第五位的数值即可。
定位内存地址的方法还有
[bx+200]这样的写法等同于200[bx]
也就是[bx+idata]寄存器的值加上一个数值可以直接找到改地址(当然还是默认的段地址)
也可以两个寄存器相加[ax+bx]来访问内存地址
举一反三自然有[ax+bx+idata]
理论上等于idata[ax+bx]???(然而并不是这样的)
要写成idata[ax][bx]
或者 [bx].idata[ax]
或者 [ax][bx].idata
(注意注意注意!这里用ax只是简单代表,但是ax并不可以用来内存单元的寻址在下面会给出)
SI和DI
si和di是远古cpu中和bx功能相近的寄存器,区别在于,sidi不能被分成两个8位寄存器使用
数据处理的两个基本问题
即
处理的数据在那个地方(寻址)
要处理的数据有多长(类型)
reg集合表示寄存器
sreg(segment)集合表示寄存器 ds\ss\cs\es
8086CPU中只有这四个寄存器可以通过[…]的方式寻址
bx si di bp
并且这几个寄存器以[…+…]的方式出现时并不能随意组合而是
bx和si
bx和di
bp和si
bp和di
在[…]中使用bp默认对应的段寄存器为ss
寻址的方法:
1.立即数(idata)
直接给寄存器赋值
mov ax,1
2.寄存器
mov ax,bx
3.段地址(SA)和偏移地址(EA)
相当于给出直接地址
mov ds:[bp]
定型的方法:
远古CPU只能处理byte(8位)和word(16位)(可能是因为CPU是16位的,算力有限)
1.通过寄存器来判定
例如ax是16位的那么就是word
al or ah是8位的就是byte
2.没有寄存器就用操作符X ptr指明内存单元的长度
比如mov word ptr ds:[0],1就是字单元
3.默认咯
push操作就默认是字
div指令
div指令是除法指令
有两个注意点:
1.除数:有8位和16位两种,在一个reg或内存单元中
2.被除数:默认放在AX或DX和AX中
格式如下
div reg
div 内存单元
比如说div byte ptr ds:[0]
含义就是(al)=(ax)/((ds)*16+0)的商
(ah)=(ax)/((ds)*16+0)的余数
总结一下:被除数是被存放在AX(16位)如果被除数过长32位那么就要放在(DX和AX中,DX为高位)
然后看除数,如果除数是8位,那么商就放在al中,余数就放在ah中
如果是16位,那么商放在AX中,余数放在DX中
一切看起来是那么的合理
伪指令dd
dd是用来定义dword(double word,双字)类型的指令
所以dd占4个字节
dup操作符
dup用来定义重复的数据
比如说db 3 dup(0)
定义了三个字节,他们的值都是0,相当于db 0,0,0
db 3 dup(0,1,2)
定义了9字节
他们是0,1,2,0,1,2,0,1,2
其他的字符串之类的都一样
dup对于开辟栈空间有着重要的作用,开辟一个200字节的栈空间只需要
stack segment
db 200 dup (0)
stack ends
即可
呼呼,开始转移指令(跳)了
可以修改IP,或同时修改CS和IP的指令统称为转移指令
只修改IP被称为段内转移,比如:jmp ax
同时修改CS和IP称为段间转移,比如:jmp 1000:0
由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移
1.短转移IP的修改范围为-128~127
2.近转移IP的修改范围为-32768~32767
远古CPU的转移指令分为以下几类:
1.无条件转移指令
2.条件转移指令
3.循环指令
4.过程
5.中断
操作符offset
offset是由编译器处理的符号,他的功能是取得标号的偏移地址
assume cs:codesg
codesg segment
start:mov ax,offset start
s:mov ax,offset s
codesg ends
end start
上面的汇编程序中,offset取得了start和s的地址,分别是0和3
jmp指令
jmp为无条件转移指令,可以只修改IP或者修改CS:IP
jmp指令需要两条信息:
1.转移的目的地址
2.转移的距离(段间转移,段内短转移,段内近转移)(我的理解其实是转移的类型)
依据位移进行转移的jmp指令
jmp shoart 标号(转到标号处执行指令)
这种叫做段内短转移IP的修改范围为-128-127
short符号指明了短转移
标号就是目的地
jmp short 的原理:
我们写的是jmp short s
但其实他所对应的是jmp 0008
这说明CPU在执行jmp时不需要转移的目的地址
但是CPU不是神仙,他必须要有东西确定jmp的位置
回忆CPU执行指令的过程:
1.从CS:IP指向的内存单元读取指令,放进指令缓冲器
2.(IP)=(IP)+所读的指令长度,然后指向下一条指令
3.执行指令。转到1
所以说jmp的机器码可以写成修改IP的指令,让操作的指令变成修改IP,修改后的IP指向跳转的地址,然后继续操作就运行了跳转处的地址。
所以jmp指令并不需要知道目标的地址,只需要知道把IP改成什么就行了,也就是告知IP的位移长度
(一点自己的想法,如果jmp都是直接给出目标地址的地址,那么机器码将会变得非常之长,如果只是简单的对IP进行操作,那么机器码会变得简短许多,运算速度也就提高了)
jmp far ptr 标号
实现段间转移,又称为远转移
(CS)=标号所在段的段地址
(IP)=标号在段中的偏移地址
jmp far ptr s 对应的机器码包含转移的目的地址
jmp reg
寄存器保存着跳转的地址
直接jmp reg就行
jmp [地址]
有两种格式
1.jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址开始存放着一个字,是转移的目的地的偏移地址
这种并不是跳转到内存单元,而是内存单元中的那个字
2.jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移目的地的段地址,低地址处是转移目的地的偏移地址
同上
jcxz指令
有条件转移,所有有条件转移指令都是短转移
所以在对应的机器码中包含转移的位移,而不是目的地址
格式:jcxz 标号
操作 (cx=0) -->(IP)=(IP)+8位位移 (这个八位位移会由编译程序给出)
相当于if((cx)==0) jmp shart 标号;
loop指令
loop指令为循环指令,所有的循环指令都是短转移
格式:loop 标号 ((cx)=(cx)-1,如果(cx)!=0,转移到标号处执行)
操作:
1.(cx)=(cx)-1;
2.如果(cx)!=0,(IP)=(IP)+8位位移
根据位移进行位移的意义
我们知道短转移,有条件转移,循环指令,用的都是位移定位IP
这种设计方便了程序段在内存中的浮动装配。
位移而不是实际地址,假如说实际地址改变了(aslr之类的奇怪的操作,那么位移定位的转移语句都可以存活下来)
CALL和RET指令
call和ret指令都是转移指令
ret和retf
ret指令用栈中的数据,修改IP的内容,实现近转移
- ip=ss*16+sp(把栈中的数据取到ip)
- sp=sp+2(归还栈空间)
retf指令用栈中的数据,修改CS和IP的内容,实现远转移
1.ip=ss*16+sp
2.sp=sp+2
3.cs=ss*16+sp
4.sp=sp+2
翻译为汇编语句就是:
ret: pop IP
retf:POP IP
POP CS
call指令
CPU执行call指令:
1.将当前的IP或CS和IP压入栈中
2.转移
call指令不能实现短转移,此外call的用法与jmp相同
根据位移进行转移的call指令
call 标号(将当前的IP压栈后,转到标号处执行指令)
CPU操作:
1.sp=sp-2
ss*16+sp=ip
2.ip=ip+16位位移
(16位位移=标号处的地址-call指令后一个字节的地址,16位位移的范围是-32768-32767,该位移由编译程序在编译时给出)
对应的汇编语言:
push IP
jmp near ptr 标号
转移的目的地址在指令中的call指令
call far ptr 标号 实现的时段间转移
CPU的操作:
1.sp=sp-2
ss*16+sp=cs
sp=sp-2
ss*16+sp=ip
2.cs=标号所在段的段地址
ip=标号所在段的偏移地址
对应的汇编语言:
push cs
push ip
imp far ptr 标号
转移地址在寄存器中的call指令
call 16位 reg
功能:
sp=sp-2
ss*16+sp=ip
ip=16位reg
对应的汇编:
push ip
jmp 16位reg
转移地址在内存中的call指令
两种格式:
1.call word ptr 内存单元地址
汇编:
push ip
jmp word ptr 内存单元地址
2.call dword ptr 内存单元地址
汇编:
push cs
push ip
jmp dword ptr 内存地址
(应该可以实现远转移了)
call和ret的配合使用
可以搞一个子程序咯,其实就是函数
mul指令
mul是乘法指令
首先两个乘数的位数必须相同远古CPU要求都是8位或者都是16位
八位的组合是:AL+内存单元 or AL+8位reg
16位的组合是:AX+内存单元 or AX+16位reg
结果 8*8 放AX
16*16 高位放DX 低位放AX
(和上次除法一样,用的都是AX和DX两个寄存器,我们有理由相信AX和DX其实是运算类的寄存器,这类寄存器专门负责数据的运算)
格式如下
mul reg
mul 内存单元
参数的传递
一个函数的值的返回,其实感觉在汇编中比在C语言中要简单,因为只需要
mov bx,数据就行了,根本不需要指针之类的操作
但是这样会产生一个问题:如何返回多个数据
C语言中有数组那么一个东西
那么在汇编中也很简单,找一块合法的内存空间就行了,把数据放在那里
(这里我觉得是否可以用栈来传递参数呢?在附录4中有答案)
用栈传递参数
那边给了一个例子
difcube:
push bp
mov ax,[bp+4]
sub ax,[bp+6]
mov bp,ax
mul bp
mul bp
pop bp
ret 4
(ret n 的汇编语法为:
pop ip
add sp,n
这里栈中有两个数,所以要归还4字节的内存空间)
寄存器冲突的问题
举一个简单的例子,在C语言中如果定义了全局变量i
那么我们在循环中使用i就会直接改变i的值,但是我们希望i的值只在这一局部改变而不影响全局
C语言中可以定义局部变量 但是汇编不行 比如说常见的技术reg cx如果主程序使用了,子程序也使用了,那么寄存器的值就冲突了
必然会导致程序的出错,所以我们需要解决这一个问题
于是我们就想到了栈,在开辟任一子程序时,我们先将子程序中要使用到的寄存器压入栈中,等程序运行完之后再将栈中的寄存器的先前的值pop回去,这样就解决了寄存器冲突的问题。
这一个操作在pwn中是非常重要的
因为我们的pwn就是通过修改ret的地址来实现劫持程序流的
标志寄存器
标志寄存器简称为flag
flag与其他寄存器不同,flag是按位起作用的
flag的1 3 5 12 13 14 15位在远古CPU中没有使用,不具有任何意义
flag的0 2 4 6 7 8 9 10 11有特殊含义
ZF标志(第6位)
ZF标志位是零标志位,记录相关指令操作的结果,如果
0 zf=1
1 zf=0
相当于 zf=1-结果
PF标志(第2位)
PF标志是奇偶标志位
他计算相关指令结果所有bit位中1的个数
如果为偶数 pf=1
如果为奇数 pf=0
SF标志(第7位)
符号标志位
结果为负 sf=1
结果为正 sd=0
这里需要讲一下补码的概念:
正数的补码就是他的二进制本身(这里只讨论二进制补码)
负数的补码是他除符号位的每一位取反后+1
这样的操作可以让计算机统一处理加减法,正数和负数
CF标志(第0位)
进位标志位
如果有一个操作后(比如相加)
结果的数据超过的寄存器能存储的最大值
之前我们说进位的数据就没了
实际上杯存储到了CF位
如果有进位那么CF=1
相同的,如果减一减向更高位借位时,那么CF=1(记录借位的数值)
OF标志(第11位)
记录是否有溢出
有of=1
无of=0
OF和CF的区别:
CF针对无符号数运算
OF组队有符号数运算
adc指令
adc是带进位加法指令
他利用了CF上记录的进位值
格式 adc 操作对象1,操作对象2
功能 操作对象1=操作对象2+CF
这个操作有啥用的
其实可以进行加法的第二步操作
小学学的竖式加法嘛
先低位相加,高位相加再加上进位
就类似于
add al,bl
adc ah,bh
sbb指令
带借位减法指令,同样利用了CF
格式 sbb 操作对象1,操作对象2
功能 操作对象1=操作对象1-操作对象2-CF
思路上和adc相似
cmp指令
比较指令
格式: cmp reg1,reg2
实际上做的就是
sub reg1,reg2
并不返回值,但是这个操作却会影响flag,通过flag标志位的变化来知道reg1和reg2的关系
(这个关系过于复杂啊,就不讨论了)
检测比较结果的条件转移指令
显然如果cmp和jmp可以联动,那么情况是很好的
因为cmp进行两种比较:有符号数和无符号数
所以对应的条件转移指令也有两类
无符号数比较条件转移指令
je 等于则跳转 zf=1 (==)
jne 不等于则跳转 zf=0 (!=)
jb 低于则跳转 cf=1 (<)
jnb 不低于则跳转 cf=0 (<=)
ja 高于则跳转 cf=0且zf=0 (>)
jna 不高于则跳转 cf=1或zf=1 (>=)
e:equal
ne:not equal
b:below
nb:not blow
a:above
na:not above
DF标志和传传送指令
DF:方向标志位
在传处理指令中,控制每次操作后si,di的增减
df=0 si,di++
df=1 si,di–
串传送指令:movsb(byte)
汇编表示:
mov es:[di],byte ptr ds:[si](远古CPU不支持这个指令,这里只是类比)
如果 df=0
inc si
inc di
入伏哦df=1
dec si
dec di
还有一个叫做movsw(word)
上面那个送字节,这个送字
所以si,di都要+(-)2
一般这两个指令都和rep连用
rep movsb
汇编表示:
s:movsb
loop s
循环次数也是根据cx来的
这样的操作可以用来传递字符串
还指明了方向,但是我们人为指定方向需要两条指令
cld 效果:df=0
std 效果:df=1
pushf和popf
这两个一看就和栈有关系,实现的功能是将标志寄存器的值压栈(出栈)
内中断
内中断相当于CPU内部发生了一些事情,这个事情的优先级是高于正在运行的程序的,所以CPU会先去执行内中断,然后再回过头来运行现有的程序
内中断的产生
1.除法错误 div产生的除法溢出(div 指令后(16位),商由ax保存,但是如果ax放不下,除法错误就产生了)
2.单步执行;
3.执行into指令;
4.执行int指令。
不同的指令有不同的中断类型码
除法:0
单步执行:1
into:4
int n就是n
中断处理程序
既然发生了中断错误,那么必须要执行处理程序,有前置知识我们知道,必须要知道程序的地址我们才可以执行处理程序
所以我们要通过中断类型码找到地址
中断类型码在内存中对应一张表
每一个中断类型码在表中都要一个地址
这样就可以找到地址了
这张表存放在内存中,是0000:0000开始的,所以这块地方是不可修改的专门放表的
中断过程
因为要修改下一步操作的地址,必然涉及到CS和IP的改变
所以会将CS:IP先压入栈中
然后最后用iret返回
iret指令的汇编表达就是
pop IP
pop CS
popf
单步中断
TF位1触发
这个和名字很相近,可以用于单步调试
只要debug把TF设置为1,那么执行一句就会停一下
响应中断的特殊情况
在开辟栈空间时假如说
赋值了SS却没有赋值SP
这时候中断就不会被响应
因为SS:SP没有被设置正确,这样会引发问题
int指令
用法是
int n
这个可以让我们访问中断表中任意一个中断
shl和shr指令
逻辑位移指令
左移(右移)若干位
最后移出的一位放入CF
最低位用0补充
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !