目录
系列文章目录
本系列博客重点在深圳大学计算机系统(2)课程的核心内容梳理,参考书目《深入理解计算机系统》(有问题欢迎在评论区讨论指出,或直接私信联系我)。
第一章 深入理解计算机系统01——计算机系统漫游_@李忆如的博客-CSDN博客
第二章 深入理解计算机系统02——信息的表示与处理_@李忆如的博客-CSDN博客
第三章 深入理解计算机系统03——程序的机器级表示
梗概
本篇博客主要介绍深入计算机系统书目第三章程序的机器级表示的相关知识。
一、程序编码
1.机器级别代码
2.不同级别的优化
3.数据格式
Tips:注意后缀(b、w、l、q对应8、16、32、64位)!!!
4.访问信息
从左至右分别为64(r开头)、32(e开头)、16、8位(l结尾)大的包括小的 非所有。
ra(b)x ea(b)x a(b)x a(b)l 有规律寄存器须记住。
寄存器中,1、2字节赋值,剩余字节不变。4字节赋值,高位4字节清零。
Tips:1、2字节赋值为低字节赋值,r8~r15为64位系统新增。
4.1 操作数指示符
大多数指令有一或多个操作数,指出源数据值与目标地址。
寻址方式:1.立即数($开头(找自己) 无$表示绝对地址(找对应地址值)) 2.寄存器(%开头) 3.内存引用(括号括起来)
Tips:比例因子必须是2的幂。
4.2 操作数示例
4.3 代码中的操作数
一维数组寻址:基址 + 比例寻址(a【n】= a【0】+ i(数据类型所占字节) x n)
二维数组寻址(假设按行存储):基址 + 比例寻址(b【i】【j】 = b【0】【0】+ i * n * m(一行多少个元素) + j * n(数据类型所占字节))
汇编代码示例(注意后缀!!):
4.4 数据传输指令
4.5 数据传输扩展示例
注意寄存器匹配性问题,mov的类型由寄存器大小决定,不可以单用mov进行不同大小寄存器互传(内存可以)。
ebx是专用寄存器,不能直接传值。
Tips:内存之间不能对拷 即不能movq (%rdi),(%rsi),应该使用中介寄存器,例如:
4.6 数据传输扩展重要示例
Tips:选择有符号扩展和无符号扩展取决于原数据的有无符号(大转小不用扩展)。
4.7 数据传输在C与汇编间的转换
内存里是对应地址的值,寄存器里直接存值。
Tips:一般默认函数的第一参数存入rdi寄存器(与位有关 edi、di、dil),第二个参数存入rsi寄存器(esi、si、sil) ,第三个放入rdx寄存器(edx、dx、dl)
Tips:注意是取寄存器(数与变量)还是寄存器对应的内存(指针值)。
4.8 压入弹出栈数据
核心原则:先进后出(地址是向栈顶指针递减)
压入弹出栈数据知识总结:
1.栈顶指针为%rsp,与mov的位无关,与系统有关。
2.sub(减)为从栈顶指针向下开辟空间并将栈顶指针下移方便mov数据(push操作),add为从栈顶指针向上寻址取出所需栈中元素(pop操作)。
3.push与pop操作均可用bwlq实现不同类型数据的进出。
5.算数与逻辑操作
常见操作如下:
Tips:自己和自己异或即初始化为0.
5.1 加载有效地址
lea(l和q与计算机系统位数有关)不访问内存,直接将寻址作为值赋给目标地址。
5.2 一元操作与二元操作
一元操作:只有一个操作数,既是源操作数,又是目的操作数。
二元操作:有两个操作数,第一个为源,第二个为目标。
Tips:lmulq 为 乘,lncq、decq为+1、-1
5.3 移位操作
C语言符号:<< 与 >> 加数字表示移动的位数
左移(shl / sal):按位左移并在右端补0
右移
① 逻辑右移(shr):按位右移在往左端补0(对无符号整数使用)
② 算术右移(sar):按位右移在往左端补最高位有效值(对有符号整数使用)
Tips:移位量只能是立即数或%rcx的%cl中!!
移位规则:
移位示例:
Tips:常数移位直接立即数移位,参数移位存入%rcx移位。
5.4 算数运算函数转换示例
对于令x = 0有以下两种方法,指令长度分别为:
1.xorq %rdx,%rdx 指令长度:3字节
2.movq $0, %rdx 指令长度:7字节
5.5 特殊算数操作
与%rax、%rdx密切相关
TIps:imull有单操作数和双操作数两种(双操作数的只取低32位),我们可以根据后面的操作数个数作出区别。
5.6 特殊算数操作转换示例
Tips:除法一般搭配扩展(有符号除法使用cqto或clto进行符号扩展,无符号除法直接将%rdx置为0进行无符号扩展)。
6.控制
除了顺序执行外,还存在着其他方式改变控制与数据流。
6.1 条件码
条件码(condition code)寄存器,其值描述最近的算术或逻辑操作的属性
•CF (Carry Flag): 进位标志(无符号数的溢出)
•ZF(Zero Flag): 结果为零标志
•SF(Sign Flag): 符号标志,结果为负时SF=1
•OF(Overflow Flag): 溢出标志,正溢出或负溢出
leaq不会改变条件码,5中常见指令均会设置条件码。
影响标志位的指令总结:
1、leal/leaq指令是计算地址,不影响标志位
2、INC(+1)/DEC(-1)设置OF和ZF,不设置CF
3、算术和逻辑指令会设置标志位
逻辑操作,CF=0和OF=0
4、移位操作 CF=最后一个移出的位,OF=0
而如下两类指令只设置条件码,不改变其他寄存器:
cmp常用于比较两数是否相等,test常用于判断某数的正负或是否非零。
6.2 访问条件码
条件码一般不会直接读取,常用方法如下:
1.利用条件码状态设置某个字节(低位单字节寄存器、一个字节的内存地址)
Tips:1.不同的后缀指明了相关条件码的组合,将一个字节设置为0或者1。
2.有条件地完成跳转
3.有条件地完成数据传送
示例如下:
6.3 跳转指令
标号: 跳转目的地址的标识(label)
1.无条件跳转:jmp + 标号直接跳转或jmp + 寄存器或内存目标中读出的的跳转目标(* + 操作数指示符)
2.条件跳转:je/jne/js/jns/jg/jge/jl/jle/ja/jae/jb/jbe
跳转目的地址:PC相对跳转、绝对跳转
跳转列表指令如下:
Tips:条件跳转只能是直接跳转,后缀与set一一对应。
6.4 跳转指令的编码
以下为汇编及反汇编示例:
链接后的反汇编版本如下:
6.5 跳转指令反汇编示例
Tips:跳转数用补码表示,注意区分正负。
6.6 用条件控制来实现条件分支
GCC编译模板如下:
goto版本C语言代码与对应汇编如下:
6.7 条件控制示例
6.8 用条件传送来实现条件分支
条件控制来实现条件分支简单但也低效,数据的条件转移在一些受限的情况下可行,更符合现代处理器特点。
简单例子及其汇编如下:
条件传送指令如下:
6.9 条件传送示例
6.10 循环
汇编用条件测试与跳转组合实现。
1.do-while循环
模板如下:
do-while示例
2.while循环
模板如下:
while示例(编译方法1)
while示例(编译方法2)
3.for循环
核心:for转while转goto
模板如下:
6.11 switch
switch根据整数索引值实现多重分支。
GCC编译器翻译开关语句(什么时候使用switch)
Tips:稀疏程度过大,建表大量存入&&loc_def,浪费空间。
C语言switch程序示例分析
switch汇编结果分析
switch转换示例
Tips:第五行为间接跳转到索引表对应内存位置(寄存器与变量寄存器一致)。
7.过程
过程调用机制如下:
7.1 运行时栈
重要:栈帧相关定义
若用到非参数寄存器(如只有一个参数却要使用%rdx、%rsi等)需要自己push和pop。
Tips:使用callq进行转移控制,原函数下一步地址压栈,返回原函数后出栈。
Tips:%rip存下一个命令所在地址。
7.2 数据传送
Tips:当函数所需函数大于6个时才需要从栈申请空间,地址随参数增加增大,参数大小向8对齐(每个参数首地址是8的倍数)。
示例如下:
Tips:一个地址为一个字节。
过程:开栈->存参(大于6)->调用函数时存函数返回地址->到函数中重复上述过程
Tips:%rsp为栈顶指针,保存了返回地址。
7.3 栈上的局部管理
Tips:call后会自动向下开空间存返回地址,sub(开空间)一定要add(删除)回来。
7.4 局部管理重要案例
Tips:局部变量起始地址和类型对齐,本函数参数从返回地址往下定义,call的函数参数(多于6)时从%rsp向上定义。
7.5 寄存器中的局部存储空间
为了保证程序的正确性(防止值的覆盖),对于某些值需要在过程中保存(也需要开空间)。
Tips:函数返回值一般保存在%rax。
7.6 递归过程
Tips:同样注意值的保存,防止覆盖。
8.数组
数组占用L(数据类型对应字节)*N(个数)的连续内存(虚拟内存)。
数组的基本原则如下:
Tips:注意是算地址还是算值。
8.1 数组示例
Tips:注意最后一题为求第几个元素,而不是地址差。
注意取地址还是求值,区分mov与lea!!!
对mov中的寄存器而言是取值,不加括号为取值,加括号为取对应地址的值(指针)。
对lea而言,是直接取括号内(内存)的地址值赋给寄存器。
8.2 嵌套数组(多维数组)
Tips:数组内存为连续的。
8.3 多级数组(指针数组)
Tips:指针的地址连续(每个指针占8字节),值为数组首地址。
嵌套数组与多级数组比较如下:
8.4 变长数组
8.5 嵌套数组示例
9.异质的数据结构
9.1 结构(体)
9.2 结构体示例
Tips:分配空间时一定要注意对齐,局部变量对齐数据类型所占字节,如上图。
9.3 联合(体)
Tips:注意大小端对数据对齐的影响。
9.4 数据对齐
Tips:可以设置不对齐,但是需要以时间换取空间。
Tips:结构体定义中空间与数据类型顺序有关(大数据类型先定义,节省空间)
10.结合控制和数据
10.1 理解指针
Tips:指针定义都会自动分配8个字节存本身。上图A2未初始化,所以读取A2【0】会出错。
10.2 内存越界引用与缓冲区溢出
x86-64 linux内存布局如下:
缓冲区溢出(越界访问了原本已使用的空间)示例如下:
缓冲区溢出定义如下:
Tips:部分库函数也存在缓冲区溢出可能(gets、scanf、strcpy、strcat等等)。
10.3 缓冲区溢出示例
反汇编观察汇编代码可以调查错误原因(字符串默认加‘\0’)
Tips:缓冲区溢出是否报错与数组位并无直接关系,与开辟栈空间(subq)的大小有关(覆盖返回地址才有可能报错)!!!
Tips:由于缓冲区溢出可能改变返回地址,故可以在栈空间内注入攻击代码完成黑客攻击。
攻击示例如下:
代码解读:要完成简单的缓冲区攻击,需要将buf的首地址覆盖原返回地址,并在buf有效位数中填入攻击内容(填充区可放入除‘EOF’与‘\n’外其他字符,gets会结束)
如本程序中将buf定义为全0,将buf地址覆盖原gets返回地址,使gets每次返回都是0。
10.4 对抗缓冲区攻击
对抗缓冲区攻击有三大方面
1.避免溢出漏洞(eg.避免使用有漏洞的(库)函数,人为检查代码)
2.采用系统级保护
2.1 随机叠加偏移
2.2 划分权限
缺点:失去部分程序灵活性。
3.编译器使用“栈金丝雀”(哨兵)
核心:在栈空间中分配一个特定值(哨兵),在回调前检查哨兵值是否改变,若改变则报错。
GCC实施:-fstack-protector
金丝雀使用实例
注意右上角的汇编代码,它的作用是从fs寄存器指向的内存的偏移为40的地方取8字节数组到rax寄存器中(可见guard值存储在gs:40处),并且将其存放在rbp+8处,故可以看出guard在当前gcc版本中起始地址位%rbp+8。
Tips:金丝雀能报的错有限,只溢出部分缓冲区(未涉及金丝雀位置)不会报错。
总结
以上便是《深入理解计算机系统》第三章——程序的机器级表示的核心知识。在第三章为计算机系统(2)中的重要内容,主要介绍了程序的各种数据格式、C与汇编的各种转换、各种不同结构的汇编表示,以及程序运行的过程、控制与风险。