操作系统基础1
编译系统分为四个阶段:
- 预处理器,#include包含的程序直接插入到程序,对预定义的常量等进行替换,还有内联函数的调用
- 编译器,将程序语言变成汇编语言
- 汇编器,将汇编语言翻译成机器语言
- 链接器,调用了printf得到可执行的hello文件
指令
- x86_64是目前笔记本和台式机最常用的处理器的机器语言,x86-64的指令长度为1-15位,设置指令的格式,将字节唯一的解码成机器指令
- x86-64的中央处理单元包含一组16个存储64位值的通用目的存储器
- 程序计数器:给出下一条指令在内存中的位置
- 整数寄存器文件:包含16个命名位置,存储64位值
- 条件码寄存器保存最近执行的算术或逻辑运算符指令的状态信息
- 向量寄存器存放一个或多个整数或浮点型的值
Linux命令行
1
2
3gcc -S mstore.c #查看汇编代码,产生mstore.s汇编文件
gcc Og -s -masm=intel mstore.c #产生函数multstore的Intel格式的汇编代码
objdump -d mstore.o #反汇编器得到代表指令的字节值GNU调试工具GDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25gdb x/14xb multstore #显示函数multstore14个16进制表示的字节,GNU调试工具GDB
gdb prog #启动gdb
quit #退出gdb
run #运行程序
kill #停止程序
break mulstore #在函数入口处设置断点
break * 0x400540 #在地址位置处设置断点
delete 1 #删除断点1
delete #删除所有断点
stepi #执行一条指令
stepi 4 #执行4条指令
nexti #以函数调用为单位,执行一条指令
continue #继续执行
finish #运行到当前函数返回
disas #反汇编当前函数
disas mulstore #反汇编当前函数
disas 0x400544 #反汇编位于地址附近的函数
disas 0x400540,0x40054d #反汇编位于地址范围内的代码
print /x \$rip #以十六进制输出十六进制输出程序计数器的值
print $rax #以十进制输出%rax的内容
x/2g 0x7fffffffffe818 #检查从地址开始的2字
x/20 mulstore #检查函数的前20个字节
info frame #有关当前栈帧的信息
info registers #所有寄存器的值
help #获取有关GDB的信息指令集是CPU用来计算和控制计算机系统的一套指令集合
- 把许多不同的指令划分成指令类,每一类执行相同的操作,操作数大小不同
- 指令可以有多个操作数:立即数 -577或0x1F;寄存器,表示某个寄存器的内容;内存引用,根据计算出来的地址访问某个内存位置
- 源操作数指定的值是一个立即数,目的操作数指定一个位置,传送指令的两个操作数不能都是内存的位置
- C语言中的地址就是指针,间接引用指针就是把指针放在寄存器中,内存引用寄存器,局部变量x通常保存在寄存器中而不是内存中
算术和逻辑指令操作
- ADD指令集是加法指令
- leaq #加载有效地址,目的操作数必须是一个寄存器
- 移位操作的第二项给出操作数,移位量可以是一个立即数或者存放在单字节寄存器%cl
- 控制指令操作
- jump #改变机器代码指令的执行顺序
- CF #进位标志 最近的操作使最高位得到进位
- ZF #零标志 最近的操作结果得到0
- SF #符号标志 最近的操作结果得到负数
- OF #最近的操作导致补码溢出
- CMP和TEST两类指令只设置条件码而不改变任何其他寄存器,CMP指令根据两个操作数之差设置条件码,不更新目的寄存器
- SET指令,将字节设置为0或者1
- 条件指令
- 条件跳转只能是直接跳转
- 跳转目标用符号标号书写,跳转指令有几种不同的编码:使用地址偏移量或者给出四个字节标志的绝对地址指定跳转目标
- 当执行计算机相对寻址时,程序计数器的值是跳转指令后面的那条指令的地址,处理将更新程序计数器作为执行指令的第一步
- rep用来实现字符串重复操作,用rep后面跟ret组合来避免使ret指令称为条件跳转指令的目标
- 处理器使用流水线获得高性能,因此使用条件传送分支会比条件控制转移有更好的性能
- 处理器采用分支预测逻辑来猜测每条跳转指令是否会执行,发生错误预测时要求处理器丢掉跳转该条指令已经所做的工作,确认分支预测错误的处罚
- 编译出来使用条件传送的代码所需的时间都是大约8个时钟周期,控制流不依赖于数据,这样可以使处理器更容易保持流水线是满的
- 循环指令,汇编中使用条件和跳转实现循环
- switch根据整数索引值进行多重分支,通过使用跳转表实现的更加高效
- 过程是一种封装代码的方式,程序用过程作为抽象机制,隐藏程序的具体实现,过程可以是函数、方法、子例程、处理函数等等
过程
过程P调用过程Q,Q执行后返回到P,具体实现为以下几个方面
- 传递控制: 进入程序Q时程序计数器被设置为程序Q的起始位置,返回时程序计数器被设置为P调用Q后面的那条指令的地址
- 传递数据: P要能够给Q提供一个或多个参数,Q要能够给P返回一个值
- 分配和释放内存: Q要为局部变量分配空间,返回前必须释放这些空间,Q调用P,Q执行过程中,P以及向上追溯到P的调用链的过程都是被暂时挂起的,当Q运行时只需要为局部变量分配新的存储空间,返回时释放
- 转移控制
- Q调用P,Q执行过程中,P以及向上追溯到P的调用链的过程都是被暂时挂起的,当Q运行时只需要为局部变量分配新的存储空间,返回时释放
- 程序可以用栈来管理存储空间,栈和程序寄存器存放传递控制和数据、分配内存所需要的信息。P调用Q时,控制和数据信息添加到栈尾,当P返回时这些信息会被释放掉
- 过程需要的存储空间超过寄存器能够存放的大小时,在栈上分配空间,称为过程的栈帧,正在执行的过程的帧总是在栈顶
- P调用Q时,把返回的地址压入栈中
- 调用可以是直接的也可以是间接的,直接调用时一个标号,间接调用是*后面跟一个操作数指示符
- 数据传送
- 大部分进程间的数据传送是通过寄存器实现的
- x86-64可以通过寄存器最多传递6个整型参数,寄存器使用的名字取决于传递的数据类型的大小,如果一个函数要传递超过6个整型参数,多余的部分通过栈来传递,参数7位于栈顶
- 寄存器中的存储空间
- 栈上的局部存储:寄存器不够存放所有的参数、局部变量使用取地址符&必须返回一个地址、某些局部变量是数组或结构
- 寄存器是被所有过程共享的资源,但是必须确保当一个过程调用另一个过程时,被调用过程不会覆盖调用者稍后会使用的寄存器值
- 寄存器%rbx、%rbp、%r12-%r15被划分为被调用者保存寄存器,P调用Q时,Q保存这些寄存器的值
- 栈指针%rsp
- 其他寄存器都被划分为调用者保存寄存器
- 递归过程调用自身,每个调用过程在栈中都有自己的私有空间
栈
- 缓冲区溢出会导致覆盖栈上存储的某些信息,随着字符串变长,下面的信息会被破坏
- 如果攻击者可以确定一个web服务器的占空间,可以设计在许多服务器上都能实施的攻击(安全单一化)
- 栈随机化(地址空间布局随机化Address_Space Layout Randommization ASLR)每次运行时程序的不同部分,都会被加载到内存的不同区域,以此来对抗一些形式的攻击
- 栈保护者(stacker protector)机制检测缓冲区越界,在栈帧中任何局部缓冲区和栈状态之间存储一个特殊的金丝雀值(canary),在程序运行时随机产生,在函数返回之前检查金丝雀值是否改变,如果改变程序终止
- GCC会试着确定一个函数是否容易受到栈溢出攻击,自动插入溢出检测,使用命令行-fno-stack-protector
- %fs:40 指明金丝雀值是用段寻址从内存读入的,段寻址机制可以追溯到80826的寻址,将金丝雀值存放在一个特殊的段中,标志为只读,攻击者不能覆盖,函数返回钱对比金丝雀值,两数相同,xorq返回0,函数正常运行,否则代码调用一个错误处理例程
- 限制可执行代码区域,保存寄存器产生的代码的那部分内存才是可执行的,其他都被限制为只允许读和写,虚拟内存空间在逻辑上被称为页,NX(No-Execute,不执行位)将读和执行模式分开,栈可以被标记为可读和可写
- 支持变长栈帧:调用alloca函数在栈上分配任意字节数量的存储,x86-64使用%rbp作为帧指针或称为基址针(base pointer)