我是靠谱客的博主 大胆绿草,最近开发中收集的这篇文章主要介绍CSAPP第三章,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在这里插入图片描述

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第三章所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部