汇编入门篇
本文最后更新于:2020年12月3日 下午
汇编入门篇
在8086CPU中,任意时刻, CPU将CS:IP所指向的内容全部当成指令来执行。
在内存中,指令和数据是没有区别的, 都是二进制信息。
只有在工作中,CPU才将有的信息当作指令,有的信息当作数据,
硬要说的话,CPU将CS:IP指向的内存单元中的内容当作指令。
在8086CPU中,所有寄存器的初始值是啥样的?AX ~ DX中的值都是0,
而ES、CS、SS、DS都是一个特定的基址值(好像是07CFH),其他寄存器中的值也是置为0
DS和数据段有关,而CS和代码段有关,修改DS可以改变数据从哪里开始读(基址),修改CS可以改变指令从哪里开始读
Debug
-r
查看改变CPU寄存器的内容
-d
查看内存中的内容
-e
改变内存中的内容
-u
将内存中的机器指令翻译成汇编指令
-a
将汇编指令写入内存中
-t
单行执行每一行的指令
r指令
怎么改变寄存器的值?
eg:-r ax
之后会显示ax原本的值,然后会提示你输入新的值。
d指令
查看指定的内存地址,直接在指令之后写上地址就行
eg:-d 0760:0000
a指令
a指令的话必须要先指定存入的内存地址(当然最好是cs:ip等代码段地址)
eg:-a 0770:0000
然后就可以输入指令的代码了,如果空着不输就直接退出了
q指令
被忽视的指令,就是用来退出debug的
eg:-q
指令的执行过程:
- CPU从所指向的内存单元读取指令,存放到指令缓存器中。
- IP = IP + 所读指令的长度,从而指向下一条指令
- 执行指令缓存器中的内容,回到第一步
转移指令(jump)的底层原理
转移指令——可以通过修改CS和IP这两个寄存器,于是决定了CPU从哪里读取代码
eg:
JMP 2000:0 ;这个编译器是不认识的,此处只是演示
于是CS被修改为2000,IP被修改为0000(注意:0被自动转换为了16进制的0000)
于是你会问了:你傻不傻啊,为啥不直接
MOV CS,2000
MOV IP,0
warning:因为这种方式是错误的(8086CPU禁止这么做)
误区:
你可能会这样想:当我执行JMP指令时,我IP尽管被修改了,但是还是要
IP = IP + 指令长度的啊!
warning:请你牢记指令执行的顺序——是先IP自加,然后才是执行指令(即修改IP和CS的值),IP自加时指令还在指令缓存器里睡着呢!
更底层的步骤
在IP将指令压入指令缓冲器之后,(IP) = (IP) + 所读指令的长度,而jmp就是更改了读取的指令的长度,从而实现指令跳转
当然,jmp这一条指令不会进行任何操作(而jz之类的可能会执行jmp这条操作)8位寄存器的算术问题
我们知道,所有的8086CPU都是16位的寄存器(不包括分开来的8位),而要是涉及到8位寄存器,则可能会产生意想不到的结果
eg:
MOV AL,C5H
ADD AL,93H
这时,AL会从C5H变成158H,然而我们知道AL是8位寄存器,所以158H会溢出成58H
有人会认为:那个进位的1跑到了AH中去了,然而我们一看AX,其值为0058H
warning:8位寄存器相加,计算机会仅仅认为是两个8位数相加了,所以AL上的溢出和其他溢出一样,不会算到AH中去。
8086CPU的设计缺陷
不能够将立即数直接送给段寄存器
eg:
mov ds, 1000h ;这会报错
其实也可以理解,毕竟段地址这么重要。。
IP是千万改不了的
这里的改不了是指用mov啥的指令去更改IP的地址
唯一能改的是其自身的自增机制和jmp类指令的跳转
与栈相关的寄存器
和stack有关的寄存器有两个:SS 和 SP
- SS
栈段寄存器:存放栈顶的段地址
- SP
栈顶指针寄存器:存放栈顶的偏移地址
于是,在任何时刻,SS: SP都指向栈顶的元素,栈的存入和导出数据本质上就是SS:SP的移动
即使是这样,我们仍然能够更改SS和SP的值,于是实现栈内存空间的转移(了解就好)
Question:
栈的push和pop都是根据栈顶指针(即SS:SP)的移动来获取和存入数据的,那我没有push数据而是直接pop数据,能否取出内存中的野数据呢?
答案是:可以的,这就要涉及到之后要讲的栈顶超界问题。
栈顶超界问题
栈空间默认是16个字节的空间,如果我不加限制地一直push会怎么样?
会超界,push的数据会覆盖掉原先的内存数据。同样,一直pop也会造成超界问题。
**解决办法?**没得😂,你得自求多福(好吧,注意编程的思路就好了)
除了代码的那些东西
段定义
To begin, you have to know something.
- 一个汇编程序是由多个段组成的,这些个段用来存放代码,数据或当作栈空间来使用。
- 一个程序至少一个段(你至少要代码吧)
- 每个段得有名字(不然谁分得清啊)
- 求你哥,千万别把这个概念和内存中段搞混了。。
formate:
段名 segment
...
段名 ends
end(不是ends)
这玩意是必写的,作为程序的结束标示。
一般格式为:
code segment
start:
...
code ends
end start
assume
这玩意是将某一段寄存器和程序中的某个段联系到一起
eg:
assume cs:codesg ;codesg是一个段的名字
loop底层原理
**原理**我们经常会用到loop这个指令,即循环指令,但仅仅就只是循环吗?或者说它会有其他条件来控制循环吗?
- (CX) = (CX) - 1
- 判断CX中的值,不为零则继续循环,为零则跳出循环向下执行。(也就是说它是有循环条件的)
看个东西:
for i in range(10)
这个python程序不跟
mov cx,10
L1: ...
loop L1
这个一样的么?
相信懂的都已经懂了( ̄︶ ̄)↗
实现大写小写字母互转
原理
大小写的字母其实差别就是一个20H,eg:
字母 | HEX | BIN |
---|---|---|
B | 42H | 0100 0010B |
b | 62H | 0110 0010B |
所以可以巧用and(与运算)
比如说可以与上1101 1111,于是正好就可以改变第六位上的二进制
或者用或,或上0010 0000
SI和DI
之前已经弄清楚了SS, CS, DS, IP, SP(几乎就是几个常用的几个寄存器),那SI和DI到底是干啥的?
- **和地址的操作有关**
- **和BX功能相近**
像这种:
mov bx,0
mov ax,[bx]
其中的BX都可以换成SI或者DI
到底有什么区别?
我们其实可以看到BX实际上是在单打独斗的,而SI和DI时常是成双出现的,这就解决了一些需要在一个逻辑中同时操作两个数据的尴尬局面(而这种局面又时常发生),比如字符串的向后复制问题
而且我们的寻址方式又增加了(老折磨人了)
[bx + si]
和[bx + di]
[bx + si + 立即数]
和[bx + di + 立即数]
DUP详解
其实我们写了很多的程序之后,都知道了用dup和缓存有关
但是我们真正了解dup吗?
- 首先你得知道,dup是duplication(复制品)的缩写
- 其次,它通常和db,dw,dd等伪指令相配合
指令 | 功能 | 相当于 |
---|---|---|
db 3 dup(0) | 定义了3个字节,它们的值都是0 | db 0,0,0 |
db 3 dup(0,1,2) | 定义了9个字节,由0,1,2重复3次构成 | db 0,1,2,0,1,2,0,1,2 |
db 3 dup(‘abc’,’ABC’) | 定义了18个字节,构成’abcABCabcABCabcABC’ | db ‘abcABCabcABCabcABC’ |
这里声明一下
db不是database,而是指字节型
dw是指字型数据,dd是指双子数据
ps:看到这里,你或许就已经知道了我们之前定义的buf其实是假的缓冲区😂,事实上,他除了名字和缓存有关,其他的实际上就是我们开辟的一个普通内存空间。
lea和offset
有时候我们会用到取出变量(这里一般指的是变量名)的地址来放入寄存器中(比如ax,bx),lea和offset就经常出现了
首先我们看一下这个为啥是错的:
mov ax,string ;这里的string是我们定义好的变量名
肯定是错的啊,这个string不能代表任何的地址含义
而:
lea ax,string
mov ax,offset string
这两行的意思是一样的,而且都是正确的。
详解
lea是将变量的物理地址送到前面一个寄存器中
offset则是一个操作符,它将取得标号的偏移地址
偏移地址再加上默认的段地址,他不就变成物理地址了吗
call和ret原理
当CPU执行call指令时,会进行两步操作:
- 将当前的IP或CS和IP压入栈中
- 转移到标号处执行指令
然后在遇到ret指令时,再将IP啥的pop出栈,转移到下一条指令去执行。
warning:需要注意的一点是,没有call指令也可以执行ret指令,这就相当于将ret指令单独执行,即将栈顶pop出数据到ip
call的段间转移
指令为:call far ptr
这事实上是将sp-2之后先将cs压入栈中,然后再将ip压入栈,其中的转移过程和jmp far ptr类似
方向标志位和串传送指令
你是否有这样的苦恼:当你在执行操作时只能够一个一个字节的进行处理,而这种执行的效率显然是低效的,你会想有没有一种可以一次执行一串的指令呢?
DF(Direction Flag)
功能:在串处理指令中,控制每次操作后SI, DI的增减。(也就是说必须得配合串处理指令才会有奇效)
- DF = 0:每次操作后si,di递增
- DF = 1;每次操作后si,di递减
串处理指令
串传送指令:movsb
它怎能做到串(string)的传送?
((es) x 16 + (di)) = ((ds) x 16 + (si)),之后si和di同时+1就行了
这里终于用到了es(extra segment),也就是说它经常和串处理有关系(当然要和ds相互配合),然后再利用si和di这对就能完成传操作了。
当然,还有其他的串传送指令:movsw(传送字节单位),这只需要将+1改成+2即可。
通过上述描述,我们肯定知道DF有多重要了(直接决定了si和di是加是减),有没有对DF进行操作的指令?
cld指令:将DF置为0(clear)
std指令:将DF置为1(setup)
rep指令
我们的串传送指令也只是一次的执行,它也得配合loop循环使用(假的串指令?),而一直写loop也有点麻烦。
rep指令能够根据cx的值,重复执行之后的指令(通常和串传送指令配合)
rep movsb
等同于:
s: movsb
loop s
位移指令
首先介绍一下SHL
SHL OPR,CNT 将OPR逻辑左移CNT位
- 将寄存器或内存单元中的数据向左移位
- 将溢出的一位写入CF中
- 最低位用0补充
warning:这里有一点需要注意的是,执行shl时,如果移动的位数大于一,必须用cl来控制循环
一些坑
- 在调用9号中断(即字符串输出)时,字符串会不停的读取字符,直到碰到$
- 调用2号中断(即单个字符输出)时,会自动输出一个字符(只是想说明和9号的终止符不同)
- 调用10号中断(即字符串输入)时,存入的第一个字符可能不在变量的偏移地址处(按常识应该在的位置),而是在偏移地址向后+2字节的地址处。
子程序坑
想要正确调用,格式是这样的:
codes segment
assume: ...
start:
main proc far
mov ax,datas
mov ds,ax
;此处输入代码
call ...
mov ah,4ch
int 21h
main ENDP
... proc near
...
... endp
codes ends
end start
输出数组的坑
当你想要利用一个定义好的变量进行输入后,你可能会纠结于怎么去得到这个数组变量里的每一个值。(因为一不留神就是一个报错)
个人建议的输出方式:
mov dl,arr[bx+2]
因为这种方法很像C语言中的指针,所以容易记忆
但是注意的是,可能有如下报错:
must be base or index registor!
这通常是你使用了arr[cx+2]
等方式,这里的base registor只有两个:ax
和bx
!(index registor就不用说了吧)
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!