文章目录
- 寄存器
- 数据格式
- mov操作
- push,pop
- call,ret
- leave,enter
- 算术和逻辑操作
- 一元操作
- 二元操作
- 移位操作
- 特殊的算术操作
- 控制
- 条件码
- 访问条件码
- 跳转
- 很好的例题
- 翻译条件分支
- 循环
- 条件传送指令
- switch
- 例
- 函数堆栈
- 递归的过程
- 数组
- 数据结构
- 结构体
- 联合
- 使用GDB调试
- 缓冲区溢出
- 使用GDB调试
- 缓冲区溢出
原文链接:原文链接
自己复习的时候,把题目写一下,会更好的理解一下
根据我的经验,画栈帧图的时候,ebp
esp
画在图的右侧比较好——栈帧从上到下地址是递减的,右侧就是表示的低地址
参考书籍:列举不分先后
- 深入理解计算机系统(原书第2版)
- 庖丁解牛Linux内核分析
- Linux内核完全解剖
寄存器
32位:
- 数据寄存器
eax
ebx
ecx
edx
eax通常作为函数的返回值
- 指针寄存器
esp
指向栈顶ebp
指向栈底
- 变址寄存器
esi
edi
-
段寄存器
es
cs
代码段寄存器ss
堆栈段寄存器ds
数据段寄存器fs
gs
-
指令指针寄存器
eip
存储下一条指令的地址
- 标志寄存器
EFlags
注意:局部变量保存在寄存器中
上面的e就是扩展,16位的变成了32位
数据格式
字节b——1字节
字w——2字节
双字l——4字节
立即数:一个数加上$
符号——$12
直接寻址:如果是寄存器,那么操作数就是寄存器存储的数——%eax
;如果是一个数(地址)的话,操作数就是地址指向的那块内存的数据——0x324
间接寻址:寄存器里的数据指向的内存数据——(%eax)
其他的形式:
2(%eax) | %eax里面的值加2的和作为地址,该地址指向的内存数据 |
---|---|
3(%eax,%ebx) | 两个寄存器里面值相加、再加上3的和作为地址,该地址指向的内存数据 |
3(%eax,%ebx,2) | %eax+%ebx*2+3的和作为地址,指向的内存数据 |
mov操作
movd
移动一个字节movw
移动一个字movl
移动2字
其他的汇编指令和这类似,下文不再论述
movl $123 %eax//立即数存入eax
movl 0x123 %eax//0x123地址处的数存入eax,比如0x123地址处的内存数据为1,那么eax就存1
movl (%ebx) %eax//ebx里面的数作为地址,把该地址处的数存入eax
//剩下的可以根据上面的数据格式就可以懂了
movs
符号扩展——补符号位
movsbw
一字节符号扩展到字,
movsbl
一字节符号扩展到两字
movswl
字符号扩展到两字
movz
零扩展——补0
同上,只不过是零扩展。
push,pop
push
压栈
pushl %eax
esp先减4,然后把eax的值压入栈顶
pop
出栈
popl %eax
把栈顶的数移入eax寄存器中,esp加4
pushl %ebp
movl %esp,%ebp
所有函数的头两条指令都是为了初始化自己的堆栈空间
call,ret
函数调用指令,调用该地址
如call 0x12345
执行该操作会做出两个动作
把当前的%eip
的值压栈,然后把0x12345
放入eip
中
为什么要这样呢?
把eip
压栈是保存旧的函数栈帧,因为eip
保存的是计算机要执行的下一条指令,使函数调用完成之后,可以继续回到原来的函数栈帧中继续执行代码。
执行指令ret
——函数返回指令
执行该指令之后,把call
压入的eip的数据(当前栈顶的第一个存储单元),重新弹回eip中*
表示不存在实际对应的指令eip
不能被程序员直接修改,只能通过专用指令(call,ret,jmp)等间接修改
把程序计数器放在整数寄存器的唯一方法
call next
next:popl %eax
leave,enter
leave
指令用来撤销函数栈帧的,等价于下面两条指令:
movl %ebp,%esp
popl %ebp
enter
指令用来建立函数栈帧,等价于下面两条指令:
pushl %ebp
movl %esp,%ebp
算术和逻辑操作
leal
加载有效地址,其实和movl
的意思差不多,但是它根本就没有引用存储器。leal
的用法
leal -8(%ebp) ,%eax
它表达的意思是把ebp-8
处数据的地址赋给eax
——eax
中的值就是ebp-8
movl -8(%ebp) ,%eax
它表达的意思是把ebp-8
处的数据赋给eax
——eax
中的值(ebp-8)
一元操作
inc x
自加1 x+1
dec x
自减1 x-1
neg x
变负 -x
not x
取补 ~x
二元操作
add
加sub
减xor
异或or
或and
与
移位操作
sal``shl
左移sar
算术右移——补符号位shr
逻辑右移——补0
特殊的算术操作
imull S
这个发现只有一个操作数,S乘以eax寄存器中的值,结果是有符号的64位的,前32位放在edx中,后32位放在eax中。mull S
和上面的一样,只不过是无符号64位的。说明i表示的是有符号imull S,D
这个就是普通的乘法,S*D->D,32位的cltd
转为4字(64位)——把eax寄存器中的值转成64位,前32位存在edx,后32位存在eax中。idivl S
有符号除法,edx:eax组成的64位数除以S,商存在eax中,余数存在edx中divl S
无符号除法,和上面一样
控制
条件码
下面两个指令只设置条件码
对应cmp指令,如果两个数相等,指令会将零标志设置为1
访问条件码
常用的三种方法:
- 根据条件码的组合,将一个字节设置为0或者1,2
- 可以条件跳转到程序的某个其他的部分
- 可以条件的传送数据
set
指令的后缀不表示操作的数大小
跳转
跳转指令,跳转到标志的地方
上面这个就是跳转到.L1
的位置
jmp
无条件跳转jmp *%eax
用寄存器eax中的值作为跳转目标jmp *(%eax)
寄存器eax里面的值作为地址,把改地址指向的内存空间的数据当作跳转目标
当执行与PC(当前指令的下一条指令)相关的寻址的时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。
jle
后面的地址是怎么得到的呢?很简单——PC的值加上0d
很好的例题
翻译条件分支
咱就是说,条件跳转是真的狗,具体怎么狗的,看下面这道题目:
注意:以下解释,如果读者看不懂,自己做一下实验即可。
总结:每对if-else
的第一个条件,其实是跳转到else执行的。(也就是说,第一条跳转指令是跳转到else的地方)这种情况初学者不是很了解。(先进行不满足条件的跳转)
其他是对源代码进行改写,变成goto语句,这样就方便理解了。
- eax寄存器存x
- edx寄存器存y
- x与-3的关系,进行设置条件码,怎么设置的咱就别管理——一定注意是后面与前面的数比较
- 该汇编是根据条件码进行跳转,我们不管条件码。结合3,它的意思是,如果x>=-3,跳转到
.L2
。但是,实际的C语言中,没有该条件。该语句其实是执行的else
语句。你品,你细品。
到.L2
之后就是正常跳转,因为这个是一组条件语句
13,14:x>2
跳转到.L5
如果不满足条件:执行15条语句,该语句其次才对应着c语言的第一条语句——val=x^y
上面是把条件中的else
中的代码完成了
从第5条汇编语句开始就是执行的if里面的代码
下面这个语句一看是比较,。。。。。。。,这咋又是一组if-else
5,6:x<=y,跳转到.L3
。也就是说,y<x执行的才是if语句
下面就不讲了,没有什么难的了。期末一定会考的哦
循环
汇编中没有循环,而是用条件+跳转组成实现的。
其他的循环会先转换成do-while
的形式,然后再编译成机器代码。
转换成goto形式比较好理解
条件传送指令
解释一下下面语句
switch
p146页,第5句话表示的跳转到
switch
的默认位置
第6句话,表示间接跳转,可以看数组那一节
第3,4句话,表示把n值减100,目的就是为了把传入的值(变成索引之后)控制在0~6范围内
.L7
中的7表示数组中有7个标号(有的值可能不存在)
画红线的地方,表示真实的下标(对应c语言的)——当然这是注解,实际是看不见的
根据上面所示101这个是不存在的,因为是默认跳转,104、106公用一个跳转,说明其中一个不存在break
语句
例
函数堆栈
什么叫做栈帧?
单个函数调用操作所使用的栈部分被称为栈帧结构(以后画右侧,看文章最前面的说明)
对于32位的x86cpu来说,通过堆栈来传参的方法是从右到左的——即在准备调用的时候,从右到左进行参数的压栈操作
如:swap(a,b)
先pushl b
再pushl a
64位的不同,这里不做讨论
函数的返回值用eax
进行保存的,如果要返回多个值,那么它保存的就是那块空间的地址
我们知道vc6.0(骨灰级编译器)在声明变量的时候,全部的变量要声明在最前面?
为什么会这样呢?
因为早期的编译器不够智能,不能智能的预留空间,所以要求程序员在最前面声明,这样在建立函数堆栈的时候,就会一个一个进行空间的申请。
我们需要确定在一个函数调用其他函数的时候,被调用者不会修改或者覆盖调用者所用到的寄存器内容。
因此InterCPU
采用统一惯例
惯例指明:
寄存器eax``edx``ecx
的内容由调用者自己负责——也就是说,被调用者,可以随意的修改
寄存器ebx``esi``edi
的值必须由被调用者来负责——也就是说,被调用者使用这些寄存器的时候,必须先进行保存,退出时进行恢复。当然寄存器ebp``esp
也遵守这个
看下面这份汇编代码:
我们发现栈帧开辟了24字节,但是我们只使用了16个字节,还有8个字节永远不会使用
为什么呢?
gcc坚持一个函数使用的所有栈空间必须是16字节的整数倍,(24虽然不是整数倍,但是看是所有,还要加上保存的ebp
和返回值,这两个总共8字节,也就是32)
一个函数的栈帧包含这几个部分:
- 建立部分——初始化栈帧
- 主体部分——执行过程的计算
- 结束部分——恢复栈的状态,以及过程返回
对于释放栈帧可以用leave
的方法,也可以用一个或者2个popl指令。这两种都可以,看见知道是什么意思就行。
递归的过程
直接看题目解释:
这里的条件判断不会那么狗,就是很直接的。
下面对汇编语句进行解释:
- ebx=x
- eax=0
- 检测
- 如果x==0,跳转到
.L3
- eax=x
- eax=x>>1
- 该行是准备参数的过程,把eax的值放入栈顶
- 递归开始
- edx=x
- edx=x&1
- eax为返回值,eax=(edx+eax)=(x&1)+rv
数组
直接上例子:结果是指针,存入eax
中;结果是整型,存入ax
中。
关键部分解释(其实是复习一下c语言的知识):
指针加一个数,那么该指针的值为:地址加上这个数乘以类型的大小
例:
short p[5]
p的地址为xp,那么p+i
的值就是xp+2*i
二维数组:
记住这个公式:
T D[R][C]&D[i][j]=xd+L(C*i+j)//L是T的类型
数据结构
这里主要说的是:结构体、联合体
结构体
我们访问结构体中的字段的时候,用的是结构体的地址加上适当的偏移
比如上面这个%eax*4
就是找到数组第i
个元素相对于数组首元素的偏移量,加上8就是相对于结构体首地址的偏移量,最后加上%edx
就是最终的地址。
看看这个例子:
联合
在使用联合的时候主要注意将各种不同大小的数据类型结合在一起的时候,字节顺序的问题是很重要的
其实和结构体差不多。直接做个例题就行,看下面的例题:
关于数据的对齐,我在C语言结构体https://code-child.cn/post/192中已经进行了说明。
通常在汇编代码中,会有这样的命令:.align 4
表示遵守4字节对齐的限制。
使用GDB调试
除了下面表中的,还可以看我之前写的文章https://code-child.cn/post/148
也可以在线查找进行调试:
- gdb 程序——开始调试
help all
缓冲区溢出
明。
通常在汇编代码中,会有这样的命令:.align 4
表示遵守4字节对齐的限制。
使用GDB调试
除了下面表中的,还可以看我之前写的文章https://code-child.cn/post/148
也可以在线查找进行调试:
- gdb 程序——开始调试
help all
[外链图片转存中…(img-SHtIiDdW-1685751716219)]
缓冲区溢出
我们在vs下面使用scanf``gets``strcpy
等库函数的时候,经常说他们是不安全的,为什么这么说呢,因为它们不进行检查栈帧空间的大小,一直写或者进行其他操作,就会越过栈帧的给它们开辟的空间,导致发生可怕的操作。
看下面的练习题