操作系统//程序的机器表示

1. 数据存储和传送

指令集架构规定了处理器状态,指令的格式,以及指令对处理器状态的影响。
X86指令长度由1-15字节组成,常用的指令以及操作数少的指令比较短,不常用的指令以及操作数多的指令比较长。

链接前和链接后的代码区别在于:1.链接后代码段的地址会发生偏移。 2.链接后会在调用函数指令中填上被调用函数的地址。 3.链接器会对代码段进行填充,优化存储器性能。

汇编代码中.开头的语句是指导汇编器和链接器工作的伪指令。通常可以忽略。

ATT和intel汇编格式:
gcc和objdump的默认格式是ATT的。gcc可以加上-masm=intel来生成intel格式的汇编代码。
intel代码有如下特点:
省略了指示大小的后缀。如mov代替movq。l ,w,b,q是AT&T汇编中用来表示操作属性的限定符。l是双字(4字节),w是字,b是一个字节 ,“q”是quad和quad是四字
使用一种不同方式来描述内存中的位置,如使用QWORD PTR [rbx]而不是(%rbx)

C语言和汇编的结合:
1.写汇编函数,让汇编器和链接器将这个函数和C语言代码结合起来。
2.利用GCC内联汇编的特性,使用asm伪指令完成结合。内联汇编描述

字和机器字长是不一样的。字是16位,机器字长则是随具体硬件决定。

x86机器有16个64位的寄存器,注意r和e是一个寄存器不同长度的称呼:
在这里插入图片描述
被调用者保存:P调用Q时,Q必须保存这些寄存器的值,无论是完全不改变,还是改变后再改回去,反正当返回P时,这些寄存器的值都要不变。一般会把传过来的参数复制到被调用者保存寄存器中,返回时参数值不变。
调用者保存则没啥特殊含义,每个函数都能修改这些寄存器,硬要理解的话,在P调用Q时,因为Q可以修改这些寄存器,所以P这个调用者有义务保护这些寄存器的数据。除了被调用者保存和rsp之外的所以寄存器都是调用者保存的。

基址寻址:基址寄存器+偏移
变址寻址:种类较多。涉及变址寄存器。用的较多的一种位Imm(a,b,c),Imm为偏移,a为基址寄存器,b为变址寄存器,c为比例系数。所获得的地址为Imm+a+b*c

mov类指令不能两个操作数都是内存,所以将值从内存复制到内存需要两条指令。
一般情况下,mov类指令指挥更新目的操作数指定的哪些寄存器字节或者内存位置,但是当movl指令以寄存器作为目的时,会将寄存器的高四字节设置为0,
在Mov指令进行数据移动时,如果涉及从较小源复制到较大目的地址的问题,有两类:

1.movz系列,后面跟源和目的的大小,做0填充,如movzbw,将做了零扩展的字节传送到字。只有较小的源b,w有零扩展传送。因为movl自带零扩展功能。

2.movs系列,后面跟源和目的的大小,做符号填充,原操作的最高位进行复制,如movsbw,将做了符号扩展的字节传送到字。所以源都有符号扩展传送。

leaq这个指令用于计算有效地址。实际上可以将其视为一个计算器,把源表达式的值算出来存放到目的寄存器内(注意leaq指令的目的操作数只能是寄存器)。可以用其来计算基址寻址和变址寻址等等的地址。
这个指令和其他的指令不同,尽管leaq中会有诸如:
在这里插入图片描述
的形式,但是他只会计算出5%rdx+7赋值给%rax,而其他指令诸如:
在这里插入图片描述
会计算出5%rdx+7并且取出该地址所对应的值,再传送给%rax

2. 条件码

CF:进位标识。可检查无符号操作数的溢出
OF:溢出标识,检查补码的溢出

对于逻辑操作,进位标志和溢出标志会被置为0。
对于移位操作,进位标志会被设置为最后一个被移除的位。溢出标志置为0。

访问条件码的一个方式SET指令:根将条件码做某个逻辑运算,将结果存储到某个字节。
使用SET指令:SET指令的目的操作数是单字节寄存器或者是一个字节的内存位置。在set指令后接一定的后缀可以获得不同的条件码逻辑运算方式。如sete,sets等等。

cmp指令会使两个数相减,根据标志位来判断大小。从后往前减。

3. 控制

跳转指令的跳转是跳转至标号,诸如.L1等等,在汇编过程中,汇编器会确定所有带标号的指令的地址,然后将跳转语句中的标号会被替换为目标指令的地址。
跳转目标形如.L1的是直接跳转,即跳转的目标是作为指令的一部分编码。
还有一种方法为间接跳转,即跳转的目标地址是存储在寄存器或者内存里的。用*表示,如 jmp *%rax,以rax寄存器的内容为跳转的目标地址。

只有jmp指令(无条件跳转指令)才能同时具有直接跳转和间接跳转,其他的诸如je,jne等有条件跳转指令只能使用直接跳转。

跳转指令的目标地址有两种编码方法:1.“绝对地址”:直接用4个字节指定目标地址。 2.PC相对地址。相对于PC给出一个偏移地址指向目标跳转地址。(PC地址即当前语句,也就是跳转语句的下一条语句的地址)
使用相对地址的好处就是即使这些指令被重定位到不同地区,也无需修改代码即可完成跳转。

rep指令是按照ECX寄存器的值,重复执行某个字符串若干次的指令。
当跳转指令直接跟在ret前时,分支不跳转直达ret会导致问题。根据AMD的说法,当ret指令通过跳转指令到达时,处理器无法正确预测ret指令的目的。所以ret指令前加上一个rep,能使得代码在AMD上运行得更快。

除去上面所说的条件跳转实现条件操作之外,还有一种通过条件传送方式实现条件操作的方法。顾名思义,条件传送是指当满足条件,将值传送到某个位置。具体的实现方法是将条件跳转的两个代码部分在分支之前全部完成计算,只是根据条件传送指令选择把哪个结果传送到结果区域。

这样的作法在流水线工作时有比较大的优势,在流水线中,如使用条件跳转指令,由于分支指令耗时,所以处理器会选择预测一个分支,以保证流水线的连续性,如果预测正确自然好,一旦预测错误,就要撤回自预测错误分支之后所做的所有工作,然后重新从正确位置取指令填充流水线。这样会导致性能惩罚。
而条件传送指令没有分支判断和跳转,只需调用条件传送指令即可完成检查,更新的过程即可。个人理解就是把原来的两大分支通过预计算简化为简单的二选一问题,这样就可以把这件事交给一条指令去做。

条件传送不一定都能使用。比如判断指针为空,如果空,则返回0,否则返回指针地址。这样的分支不能使用条件传送。条件传送会先执行两个分支,会导致访问空指针的错误。

循环结构有两种,一种是跳转到中间结构,一种是guarded-do形式。
两种策略的循环主体都是
.loop:

if()
jmp loop 的形式
后者在前面首先执行一个跳出检查(while)。使用GCC的优化等级-O1会采用这种策略。前者先执行的是一个跳转(while,跳转到条件判断部分)或者直接执行(do while)。至于for循环可以转化为while循环,注意不是do while,因为for实现要做一次检查。

switch语句的高效性通过跳转表实现,跳转表的每个元素都是一个指向代码位置的指针,对于多个编号指向同一代码地址的情况,简单的将里面的代码表项置为一样的地址,对于编号缺失的情况,将对应的代码位置置为默认地址。

4. 过程

过程是一种很重要的抽象,提供了一种封装代码的方式,用一组指定的参数和可能的返回值实现某种功能,隐藏具体实现,同时提供清晰的接口定义。可以在程序的不同地方调用该过程。常见的过程形式有:函数,方法等等。

与栈相关的重要概念:
栈帧:过程在栈上分配的空间,称为这个过程的栈帧。当前正在执行的过程的栈帧总是在栈顶。在调用函数时,调用者会将返回地址先压入自己的栈。调用者在调用之前会把参数复制到寄存器。通过寄存器可以给被调用者传递6个整数值,如果需要传送更多参数,调用者需要在自己的栈帧内预先存好多出来的参数。注意参数的顺序,所有参数数据大小都向8字节的倍数对齐,不足8字节的,占用地址的低n位:
在这里插入图片描述
数组在存储过程中,数组头部地址作为数组地址,且数组头朝着栈顶存放。

栈结构如下:
在这里插入图片描述
P中的call指令会将call的下一条指令的地址压入栈。
Q中的ret指令会把返回地址从P的栈帧中弹出。

为什么局部变量必须在内存里面?
1.寄存器数量不够用
2.对局部变量可能会有使用&取址的行为,使用寄存器不能做到。
3.某些局部变量是数组或者结构,所以需要能通过对数组或者结构的引用访问到。
注意跟在局部变量后的是传的参数,而参数需要是8字节整数倍。所以会出现如下结构:
在这里插入图片描述
x1~x4为局部变量,参数7和8为第7和8个参数。
汇编语言访问二维数组,是通过数组基址、数组元素大小、数组下标计算出的地址来访问数据的。
在这里插入图片描述

这里有一个优化点:上述计算地址的过程使用了两次乘法,乘法在某些处理器上是非常占用资源的。所以编译器可以对其进行优化,不使用高级语言的索引访问,而是使用指针访问地址的方法,每次用地址的移动(加法)代替索引移动。
如下图:
在这里插入图片描述
原来取得两个地址需要4此乘法,现在不需要乘法。

二维数组的维度在传入函数前还不知道怎么办?可以采用如下形式:
在这里插入图片描述
n动态的指定二维数组的维度。

5. 结构体与联合

指向结构体的指针是结构第一个字节的地址。

结构体:把不同类型的对象聚合到一个对象内
联合:允许多个类型引用一个对象。
二者可以看成是类型和对象间的一对多和多对一的关系。
联合所占用的空间等于内部最大的数据类型的空间,所以数据类型公用一块内存,相互覆盖。有共同的基址。在没有新的赋值或者改变时,以不同类型的方式读取联合,所得到的位是完全一样的。

无论数据是否对其,x86-64硬件都能正常工作,为了提高性能,对齐原则是任何K字节的基本对象的地址必须是K的整数倍。

在这里插入图片描述
结构体对齐时,结构体中的每个数据类型都要遵循统一的对齐间隔。

6. 指针

指针

  1. 每个指针都对应一个类型,但是在机器代码层面是没有类型之分的,类型的存在是为了帮助程序员避免犯错。void* 表示通用类型指针,malloc函数的返回值究生void*类型,然后需要使用强制类型转换转成需要的类型。对指针进行强制类型转换不会改变指针的值,只改变指针的类型。
  2. 每个指针都有一个值,特殊值NULL(0)表示指针没有指向任何地方。
  3. 指针可以指向函数。函数指针的值是该函数的机器代码的第一条指令的值。使用示例如下:
    在这里插入图片描述
    在这里插入图片描述

7. 缓冲区溢出

缓冲区溢出:
在这里插入图片描述
当对buf复制的字符超过8个时,就会产生溢出:
在这里插入图片描述

缓冲区溢出的危害:

1.会导致栈数据的破坏。可能会使用本不该使用的内存或者覆盖返回地址。
2.覆盖返回地址会导致攻击者重定向程序控制流到攻击程序。

对抗缓冲区溢出:

  1. 栈随机化。在过去,使用同一套程序和操作系统的系统而言,栈的位置都是固定的。这导致了许多操作系统都易受到同一种病毒的攻击。解决方法:在程序开始前,分配一段不使用的随即大小的栈空间,使得每次执行时的后续地址都不一样。这样的话,攻击者就不知道自己的攻击代码被加载到什么位置,就无法通过ret到这个位置完成攻击。
    LINUX中采用了一种名为地址空间布局随机化(ASLR Address-Space Layout Randomization)的方法。在每次运行时,程序的不同部分诸如代码,库代码等等会被加载到内存的不同位置。
    空操作雪橇可以破解这种随机化,在实际攻击代码前加入一长串Nop指令。比如随机化范围大小为2^23 ,使用 2^15大小的雪橇,只需要枚举 2^8即可完成攻击。这就是类似于A长度的直线范围内找到一个B长度的直线,可以考虑把A分为长度为B的小段,每次只需要检查段首是不是存在B即可,要检查的数量就是A/B。
  2. 栈破坏检测–金丝雀值。GCC自带,需要用命令行参数-fno-stack-protector取消。会在栈帧中任何局部缓冲区和栈状态之间存储一个特殊的值,该值是程序运行时随机产生的。在程序返回和恢复寄存器状态时,会先检查这个值是否被修改,如果是,则程序异常终止。金丝雀值使用段寻址,存储在一个特殊的段中,该段被设置为只读。
  3. 限制可执行代码的区域。消除攻击者向系统插入可执行代码的能力。典型的程序中,只有保存编译器产生的代码那部分内存才需要是可执行的,其他部分被限制为只允许读和写。

8. 变长栈帧

对于变长的栈帧。如在函数内声明一个局部的变长数组时。需要用到%rbp寄存器。将其作为帧指针或者基指针(base point)。在调用函数时,先将之前%rbp的值压入栈,然后将%rbp指向该值。在使用过程中,通过相对%rbp的偏移值访问数据。在调用结束时,首先把栈顶指针%rsp设置为保存%rsp值的位置,然后把该值弹出赋值为%rbp。存储结构如下:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值