概述
1.c语言代码->汇编代码
inter cpu中有16个寄存器
caller-saver/callee-saved Register
调用者保存寄存器的值
或者被调用者保存寄存器的值
汇编器将机器代码翻译成机器代码(二进制代码)
反汇编器就将机器代码(二进制代码)翻译成汇编代码
c语言汇编和反汇编器产生的代码对比如上:
Register:
各个寄存器的功能:
rax保存函数返回值 return value
rdi,rsi,rdx,rcx用来传递函数参数(这点很重要)
rsp保存栈顶指针
Instruction(指令)
操作数和操作码要匹配
64位处理器的限制:
源位置和目的位置不能都是内存中的位置
如果要将内存到内存,
mov指令的大小和寄存器或者数的大小一定得是匹配的
操作数和操作码要匹配,目标数可以扩展
当movl的目标操作数是寄存器时,它会把该寄存器的高4字节设置为0
这时64位处理器的规定,即任何位寄存器生成32位值的指令都会把该寄存器的高位部分设置为0.
当源操作数位小于目的操作数时,需要对目的操作数剩余的字节进行零扩展或者符号位扩展
一个程序的执行过程:
如果cpu要执行 c=a+b
首先,cpu要先从内存中取出数据a和b,然后将其送到cpu的寄存器中,
寄存器是64位,如果要保存64位的数据,就需要用到寄存器的64位
如果只需要保存一个int类型的数据(32位),就只需要用到32位。
如果只是short类型,则只需要用到低16位。
如果用到64位,就用rax表示
32位就用eax表示,32位用ax表示,16位al表示。
现在来将C语言中指针的内容与寄存器的知识联系起来:
Stack
程序栈本质是内存中的一个区域
栈的增长方向是从高地址到低地址
pushq指令等效于下面两个指令
执行过程就是先将rsp(栈顶指针减8),然后再从内存中找栈顶指针指向的内存地址,然后将值push进去。
算术逻辑操作和计算的指令
leaq:它的功能是加载有效地址
x86-64位处理器上,地址长度都是64位
一定要注意 leaq和mov的区别
如图上的例子,leaq的意思是将5x+7这个地址保存到寄存器rax当中。
而如果是mov,则是将内存中5x+7位置的值保存到寄存器rax当中。
leaq还可以用来表示加法和有限的乘法运算
左边的代码就是通过右边的指令实现的
一元操作:
加,减,负,补
二元操作
加,减,乘,异或,或,与
shift operation
移位量可以是个即时数,也可以是保存在cl中的数,但只能用寄存器cl来保存
乘以16,就相当于左移4位
条件循环语句的实现
在cpu中,不仅有寄存器,还有各种运算单元。
比如说处理加减的算数逻辑单元,ALU。
ALU是如何工作的呢,如图所示,ALU先从寄存器中存相应的值
然后在再ALU中进行相应的计算,然后将计算的值保存到寄存器中。
ALU除了执行算术和逻辑运算指令外,还会根据运算的结果去设置条件码寄存器。
条件码寄存器:
是由cpu来维护的,它描述了最近执行操作的属性
CF:Carry Flag 进位标志,当CPU最近的操作产生进位时,CF会设置为1,可以用来检测无符号数操作的溢出。
ZF:
当最近结果为0时,ZF会被置1.
zhi
条件码寄存器的值是由ALU在执行算术和运算指令时写入的。
cmp和test指令
cmp根据两个操作数的差来设置条件码寄存器
cmp和sub的区别就是:
sub既改变操作数的值,又设置条件码寄存器
cmp只设置条件码寄存器,不改变操作数的值
test指令和and指令类型
因为指令的执行是独立的,这一条指令不知道上一条指令的执行结果,如果需要根据上一条指令执行结果来执行命令,就需要用条件码来保存。
1.取出a和b的值,然后根据a和b的值做比较,设置zf的值(comp的功能)
2.sete(后缀e表示equal):是根据ZF的值对al进行赋值
if ZF=1,将al的值设置为1
如果ZF=0,将al的值设置为0
3.movzbl指令对 al进行0扩展,再将值赋给eax。
1.com指令,反正com指令是一个指令,就是将拿a-b,然后执行com指令后相应的符号位寄存器就都会发生变化,比如SF标志位,OF标志位,然后我们就可以通过这些标志位的值来进行判断。
2.setl(l表示less的意思,表示在小于时设置)
3.它是如何去判断小于的呢:
4.将al的值(表示小于还是不小于)赋给eax。
综上所述:
这么讲,
1.com就是将两个数相减然后根据结果去设置状态码。
2.setl,sete %al,就是根据两个值是否相等还是是否大于小于去设置al的值。
(当然,如何判断相等或者大于小于,是它内部实现,不如等于就看ZF标志位,大于小于就得看ZF和OF的异或结果。
跳转指令和循环
理解一下上面的代码:
1.cmp会根据rsi,rdi的值去设置SF,OF的标志位
2.j1.L4 根据SF和OF的异或结果判断是继续顺序执行还是跳转到.L4的代码去执行。
Jump Instructions 跳转指令
现代处理器中,执行效率比较低。
替代方法:使用数据的条件转移来代替控制的条件转移。
基于条件传送的代码会比基于跳转指令的代码效率高,是因为现代处理器是通过流水线获得高性能。
循环结构
c语言中提供了三种循环结构,do-while,while,for语句
循环语句是通过条件测试与跳转的结合来实现的。
当n的值大于1时,会一直跳转执行,小于1时,就往下执行
switch语句:
将n的值与6的比较,如果n大于6的话,就跳转到L8 default的语句去
对于case0-case6,跳转表:
函数
函数p调用函数q的例子中,当函数q正在执行时,
函数p以及相关调用脸上的函数都会被挂起
当函数执行所需要的存储空间超出寄存器能够存放的大小时,就会借助栈上的存储空间,我们把这部分存储空间称为函数的栈帧。
所以栈帧是以一个函数为单位的。
对于函数p调用函数q的例子,包括较早的帧,调用函数p的帧,正在执行函数q的帧
一个是之前较早的帧,一个是函数p的栈帧,一个是函数q的栈帧
当函数p调用函数q时,会把返回地址压入栈中
该地址指明了当函数q执行结束返回时要从函数p的哪个位置继续执行
这个地址的压栈操作并不是由push来执行的,而是由call指令来操作的。
函数调用与返回的操作:
返回地址
比如以上的指令:
1.callq 741 这个指令会将rip寄存器的值设置为741,意思就是下一个指令执行的位置就是741.
2.call指令还会同时将call指令的下一条指令,700压入栈中(rsp),
3.当call指令执行返回时,将会自动从rsp中取值700,将其设置为rip的值,也就是下条指令执行的地址。
参数传递
如果一个函数的参数数量大于6,超出的部分就要通过栈来传递。
参数1-参数6可以通过相应寄存器来传递
实例:
前六个参数通过寄存器来保存。
后面两个参数通过栈来保存。
因为栈顶默认存放返回地址,所以a4和a4p分别存在距离栈顶8和16的位置。
这里有一点需要注意的是:
当栈作为参数传递时,比如说a4是char类型的(4字节),但同样为其分配8个字节的空间。
寄存器的使用有特定顺序:
局部变量需要栈的空间,是不需要八字节对齐的。
传递参数就需要(所以都为其分配8个字节或者八个字节的倍数)
在程序执行过程中,寄存器是所有程序共用的。
rbp和rbx要被调用者保存。
递归调用一个函数本身和调用其它函数是一样的
每次调用都有自己函数的状态信息
栈分配与释放的规则和函数调用返回的顺序也是匹配的
当N的值特别大时,不建议使用递归调用。
数组
p指向的是char类型的指针,q是指向int类型的指针
当p+1时,会向右移动一个单元,q+1时,会向右移动四个单元,这个具体是根据引用类型的不同来决定的。
同样是c的知识,数组的名字代表的是数组首个元素所在的地址。
所以要取数组E的第三个元素,可以是E【2】,或者是*(E+2)
(E代表的是首地址,所以E+2代表E后面第二个地址,*(E+2)代表取E后面的第二个地址的值)
Nested Arrays
结构体
结构体的声明:
无论是单个变量还是数组元素,独是通过起始地址加偏移量的方式来访问的。
该结构体并不是占9个内存空间。
为什么呢?因为变量j是int类型,占4个字节,它的起始地址必须是4的倍数,因此,编译器会在变量c和变量j之间插入一个3字节的间隙。
这样,变量j相对于起始地址的偏移量就为8,整个结构体的大小就变为12字节。
比如说double c 八个字节长,所以它的起始地址需要是8的倍数,所以需要在a,b 后面补上7个字节使得其前方有16个字节(8的倍数)
我们如果知道两个字段的使用是互斥的,那么可以将这些字段声明为一个联合体。
强制类型转换时,联合体的应用:
缓存区溢出
栈帧中会保存程序执行所需要的重要信息,例如:返回地址,保存的寄存器的值。
在c语言中,对数组的引用不会进行任何的边界检查,如果对越界的数组进行写操作,就会破坏存储在栈中的状态信息。当程序使用了被修改的返回地址时,就会导致严重的错误。
小例:
1.定义了一个char类型的数组
2.get函数是将输入的字符转移到制定的位置(buf)
3.但是有个问题,就是char数组并不会判断是否越界,如果输入的字符串超出了栈为其申请的缓存空间,就会缓冲区溢出,就可以溢出到函数的返回跳转指令那里,导致一些很严重的问题。
防止缓冲区溢出的方式:
Thwarting Buffer Overflow Attacks
1.Stack Randomization
2.Stack Corruption Detection
3.Limiting Executable Code Regions
1.栈随机化
栈的位置在每次程序运行都不同
2.栈破坏检测
在缓冲区和栈保存的状态值之间存储一个特殊值
这个特殊值被称为canary 金丝雀值
每次程序运行的时候随机产生的
fs:40 可认为是内存中的一段只读的地址,存储的就是金丝雀值
右边的xorq 就是在每次程序运行前判断一下canary有无被修改
3.消除攻击者向系统中插入可执行代码的能力
限制哪些内存区域能够存放可执行代码
以前,x86处理器将可读和可执行的访问控制合并成一位标志
所以可读的内存页也是可被执行
后来,处理机的内存访问位可分为 可读和可执行。
最后
以上就是大胆绿草为你收集整理的CSAPP第三章的全部内容,希望文章能够帮你解决CSAPP第三章所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复