计算机系统基础
计算机系统基础
第一章 计算机系统概述
1. 计算机基本执行过程
1.1 冯·诺依曼机基本结构
绝大部分通用计算机的硬件基本组成仍然就有冯·诺依曼结构特征
图解:
- 主存储器(主存,内存):存放指令和数据
- 算术逻辑部件(ALU):进行算术逻辑运算的核心部件,可以对输入端A和B进行不同的运算,得到结果F
- 控制部件(CU,控制器):自动逐条取出指令并进行译码(将二进制的操作码翻译成对应的操作)
- 通用寄存器(GPRs):临时存放从主存取来的数据或者运算结果
- 标志寄存器(SF):存储ALU运算产生的标志信息
- 指令寄存器(IR):临时保存从主存取出的指令
- 程序计数器(PC):自动按序读取主存中的指令,并且在执行指令的过程当中计算出下一条指令的地址并保存
- 中央处理器(CPU):通常把控制部件、运算部件和各类寄存器互连组成的电路
- 主存单元地址:主存中每个单元的编号(地址)
- 总线:连接不同部件进行信息传输的介质
- 主存地址寄存器(MAR):CPU送到地址线的主存地址先存放在这里 (MAR只会从CPU内部取地址,而不会从内存中取)
- 主存数据寄存器(MDR):发送到或者从数据线取来的信息放在这里(包括指令和数据,双向过程,分别对应load 和 store)
1.2 程序和指令的执行过程
指令的概念:用0/1表示的序列,指示CPU完成一个特定操作。
常见指令:
- load
- store
- add
- mov
指令通常被分为若干字段,一般为操作码字段和地址码字段
在模型机上执行 $z = x + y$ 过程:
程序执行过程:
根据PC取指令
指令译码,PC<-PC + 1
取操作数并执行
送结果
循环1-4
图示:
指令各执行阶段还包含微操作,需要相应的控制信号:
时钟信号:在每条指令的执行过程中,所包含的微操作具有先后顺序关系,需要定时信号进行定时。
时钟周期:时钟信号的一个宽度
2. 程序的开发和运行
2.1 程序设计语言和翻译程序
机器语言,机器指令:0/1
汇编语言,汇编指令:方便阅读和编写,需要汇编成机器语言
高级语言:阅读性和编写性更好,需要翻译
翻译程序:
- 汇编器(assembler):汇编语言-》机器语言
- 解释器(interpreter):逐条翻译成机器语言
- 编译器(compiler):源程序-》汇编语言或机器语言目标程序
2.2 从源程序到可执行文件
预处理阶段:嵌入include文件,宏展开
编译阶段
汇编阶段
链接阶段:将多个可重定位目标文件(.o .obj) 和标准函数库中的可重定位目标文件合并成为一个可执行文件.
如hello.o + printf.o -> hello.exe
可执行文件的启动和执行:
概括:
- shell程序会将./hello的每一个字符逐一读取到CPU寄存器中,再通过存数操作存储到主存的缓存中
- shell调用操作系统内核中的服务例程,加载磁盘中的hello.exe到存储器
- 逐一执行程序指令
- 将结果打印到显示器上
3. 计算机系统的层次结构
3.1 计算机系统抽象层次的转换
上层结构是下层结构的抽象,下层是上层的环境。
指令体系结构(ISA):
- 定义了一台计算机可以执行的所有指令的集合
- 每条指令规定了计算机执行什么操作(如不同ISA规定的乘法指令所消耗的时钟周期数不同)
- 所处理的操作数存放的地址空间和操作数类型
更详细的规定如下:
可执行的指令的集合,包括指令格式、操作种类以及每种操作对应的操作数的相应规定;
指令可以接受的操作数的类型;
操作数所能存放的寄存器组的结构,包括每个寄存器的名称、编号、长度和用途;
操作数所能存放的存储空间的大小和编址方式;
操作数在存储空间存放时按照大端还是小端方式存放;
指令获取操作数的方式,即寻址方式;
指令执行过程的控制方式,包括程序计数器、条件码定义等。
3.2 计算机系统的不同用户
4. 计算机系统性能评价(重点)
4.1 计算机性能的定义
- 吞吐率:单位时间完成工作量,相当于带宽
- 响应时间:从作业提交到作业完成所用的时间,类似执行时间,包含CPU时间和其他时间(如IO操作,磁盘读取)
- CPU时间:CPU用于本程序执行的时间
- 系统性能:包括CPU性能和其他性能
- CPU性能:只包含CPU执行时间
- 时钟周期:时钟信号的宽度(clock cycle),一条指令可能需要多个时钟周期来完成
- 时钟频率:CPU的主频(clock rate),时钟周期的倒数
- CPI(Cycles Per Instruction)执行一条指令所需的周期数
$$
程序总时钟周期数=\sum\limits_{j=1}^{n}(CPI_{i}\times C_{i})
$$
CPI可用$F_{i}$(第i种指令在程序中所占比例)表示:
$$
CPI = \sum\limits_{j=1}^{n}(CPI_{i}\times F_{i}) = 程序总时钟周期数\div 程序总指令条数
$$
但是CPI并不是衡量CPU性能的唯一标准,因为不同CPU的时钟周期也可能不相同。
第二章 数据的机器级表示和处理
1. 数制和编码
1.1 进制数之间的转换
常见的二进制长度:
1byte:8位二进制,可表示的无符号数范围:
0000 0000 ~ 1111 1111 即 0 ~ $2^{8} - 1$。
unsigned int: 4个字节,表示范围:
0000 0000H ~ FFFF FFFFH 即 0 ~ 4294967295
1.2 整数的表示
无符号整数和有符号整数的区别:
- 无符号整数,默认使用所有二进制位来存储实际数据。对于32整型,其长度为0~$2^{32}-1$。
- 有符号整数,最高位(MSB)或者最低位(LSB)作为符号位,因此表示范围比无符号少,对于32位整型,其长度为$-2^{31}$~$2^{31}-1$。
不同的编译器对于数据类型的判断是不一样的。
1.3 浮点数的表示
浮点数的IEEE 754 标准:
对于32位单精度格式:
尾数为23位,其中有一个隐藏位1在小数点之前。
eg:一个浮点数的尾数为1011 1100 1110 0000 … , 则其实际的尾数为1.1011 1100 1110 0000…
阶码为8位,采用移码的形式,其中偏置常数为127,可表示的阶数的范围为(1~254)- 127(0和全1有特殊意义)。
符号位1位,判断符号。
浮点数的解释:
记忆:
在尾数为全0的情况下,阶码的两个极限(0和255)分别表示0和无穷大,正负看符号位
当尾数不为0时:
- 阶数为255,则为无定义数
- 阶数为0,则尾数失去隐藏位1,然后向0过渡。
当浮点数和整数的长度相同时,他们表示的数的个数没有改变(每一个二进制数唯一对应一个真值),但是浮点数的范围扩大了,所以浮点数越大,两个相邻二进制数的真值的差值越大,即尾数后面的数字被计算机忽略掉了。
2. 数据的基本运算
2.1 二进制位的计算
按位运算和逻辑运算
按位运算 :
二进制的每一位数字逐个进行运算。常见的有: I , &, ~, ^(异或)。
按位运算常用于进行掩码操作,可以提取重要的位,然后对这些位进行置1,清0,或者测试该位是否为1或0.
逻辑运算:
非数值运算,其操作数只有两种逻辑:True 和 False。常见的有:||, &&, !。
左移运算和右移运算
逻辑移位:
移位后缺失的位直接补0。
算术移位:
右移时,最高位会根据符号位进行补充,以保证带符号数的符号不会变化,其他情况移位后缺失的位直接补0。
位扩展运算和位截断运算
0扩展和符号扩展:分别对于有符号数和无符号数而言
位截断:直接舍去多出来的位数,截断具有一定的风险,因为可能导致数据精度或者符号发生变化,也可能彻底改变数值。
2.2 数据的存储
最低有效位(LSB):数字的最低位
最高有效位(MSB):数字的最高位
大端方式:数据的最高有效位存储在小地址单元中,IBM采用这种方式
小端方式:数据的最低有效位存储在小地址单元中,Intel使用,“低对低,高对高”,低字节对应低地址。
2.3 整数加减运算
无符号整数和带符号整数的加减运算电路是完全一样的,都在整数加减运算器中实现。
MUX是一个二路选择器,Sub控制信号会告诉MUX此时所做的是减法运算,那么MUX会选择把Y按位取反后进行输出,同时,控制端Sub作为低位进位送到加法器。当Sub为1时做减法,实现x-y = X + $\overline{Y}$ + 1, 当Sub为1时做加法,实现x + y = X + Y。
加法器本身无法判断数据是否溢出,所以需要通过输出得到的标志寄存器来判断。
OF:
用在有符号数的加减法中,为1则表示溢出
常见溢出情形:
两个正数相加(一个正数减一个负数)变成负数,此时$C_n=0,C_{n-1}=1$,OF=1,发生溢出
两个负数相加(一个负数减一个正数)变成正数,此时$C_n=1,C_{n-1}=0$,OF=1,发生溢出
SF:
直接取最高位的数字,1表示负数
ZF:
0表示该数不为0,1表示该数为0
CF:
用在无符号的加减法中,做加法时,为1表示需要进位,做减法时,为1表示需要借位
判断的标准是输入位(减法为1,加法为0) 异或 输出位(第n + 1位)
无符号数加法公式:
无符号数减法公式:$[-y]_补=2^n-y$
带符号数加法公式:
带符号数减法公式:
可以类比于小时的运算规则。
总结
在机器级层次上,并不区分操作数是什么类型,只是编译器根据高级语言程序中的类型定义对机器数进行不同的解释而已。
有些语言为了避免无符号和有符号的麻烦就不支持无符号类型,比如Java
2.4 整数的乘除运算
整数乘法
尽管两个n位整数相乘会得到一个2n位 的整数,但是在许多高级语言中高n位的数据会被截断,因此整数的乘法可能产生溢出问题。
溢出判断
- 对于n位无符号整数而言,如果高n为全为0,则没有发生溢出;
- 对于n位带符号整数而言,如果高n位全部等于低n位的最高位,则没有发生溢出;
另一种人为的判断方式是,假设x * y = p, 只要验证p / y 是否等于 x 即可确认是否发生了溢出 。
整数除法
只有在最小数除以-1时才会发生溢出,除法由于需要试商的缘故,所以耗费的时钟周期会很多。
2.5 常量的乘除运算
乘法运算
由于一次乘法运算需要10个左右的时钟周期,因此编译器在处理常量的乘除运算时,会用移位,加法和减法来代替乘法运算,这样可以 大大缩短时钟周期,例如x * 20 => (x << 4) + (x << 2)。
除法运算
- 无符号整数:直接逻辑右移,高位补0,商朝0方向舍入。
- 有符号整数:如果为正数,则处理方法同无符号整数;如果是负数,如果直接移位的话,商会朝着远离0的方向舍入,此时需要加上偏移量$2^k - 1$(k是移位数),这样,当舍去的二进制位全零时不会受到影响;当舍去的二进制位不为全零时,被除数偏移后一定会发生进位,此时再向右移位得到的结果就是朝0方向舍入的商了。
2.6 浮点数运算
2.6.1 加减运算
对阶
对阶的目的是使两个数的阶相等,以便位数可以相加相减。
对阶的原则:小阶向大阶看齐,阶小的尾数右移,右移的位数等于两个阶的差的绝对值
注意:在采用IEEE754标准时,需要将隐含的1右移到小数部分
阶差的计算公式
$$
[E_x - E_y]_补 = [E_x]_移 + [-[E_y]_移]_补
$$
例:尾数加减和规格化
对阶之后需要对尾数进行加减,得到的结果如果不符合标准需要进行左规和右规。
左规:遇到结果为0.0…01bbb的情况,让阶码逐次减1,直到整数部分为1。如果数据本身是0,则不需要处理
右规:尾数右移一位,阶码加一
尾数的舍入处理
为了保证计算具有更高的精度,IEEE754标准规定所有浮点数运算的中间结果右边都必须至少保留两位附加位,即保护位和舍入位。
附加位的作用:用以保护对阶时右移的位或运算的中间结果
就近舍入的默认方式:如果附加位小于1/2,截断;如果大于1/2,进位;等于1/2,取最近的偶数。
阶码溢出判断
阶码上溢:阶码为全1,此时产生异常或者把结果置为无穷大。
阶码下溢:阶码全为0,此时结果为非规格化形式,如果也不能表示则近似为0。
可以总结如下:
第三章 程序的转换及机器级表示
计算机的指令有微指令,机器指令和伪(宏)指令之分。
微指令:微程序级指令,属于微体系结构,硬件范畴
宏指令:由若干机器指令组成的序列,软件范畴
机器指令:位于两者之间,可由汇编语言来表示,属于指令集体系结构(ISA)
他们分别对应着计算机系统的层次结构中的中间三个结构。
1. 程序转换概述
1.1 机器指令及汇编指令
机器语言程序是一个由若干条机器指令组成的序列。
汇编语言程序将机器语言中的操作码或者操作数转换为汇编助记符。
操作码字段:指出指令的操作性质
立即数字段:指出操作数或者偏移量
寄存器编号字段:给出操作数或操作数地址所在的寄存器编号。
1.2 指令集体系结构
Instruction Set Architecture,ISA,对使用硬件的软件屏蔽底层硬件的实现细节,将物理上的计算机硬件抽象成一个逻辑上的虚拟计算机,成为机器语言级虚拟机。
正因为不同的硬件其指令集体系可能不同,因此同一个程序在不同的系统中运行时不一定能够兼容。可理解为在虚拟机A上运行的程序不能再虚拟机B上运行,但是Java程序不同,Java天生就运行在自己的虚拟机中,它不需要依赖于每个计算机的指令集体系,因此可以一次编译,到处运行。
1.3 生成机器代码的过程
- 预处理:展开include和宏
- 编译:将c文件编译成汇编语言程序,使用的是AT&T格式,目的操作数在右而源操作数在左。
- 汇编:将汇编程序转变成机器语言程序,一般是.o文件,可通过objdump -d filename进行反汇编
- 链接:将多个可重定位目标文件以及库函数链接起来,形成可执行文件
AT&T格式:
可执行文件的存储器映像
2. IA-32指令系统概述
2.1 数据类型及其格式
IBA规范中C语言基本数据类型和IA-32操作数长度之间的对应关系。
2.2 寄存器组织和寻址方式
IA-32指令的操作数有三类:立即数,寄存器操作数,存储器操作数
定点寄存器:没有专门用途的可以存放各类定点操作数的寄存器。
通用寄存器编号
寻址方式
对于数组元素的访问一般采用“基址加比例变址”的寻址方式。
2.3 机器指令格式
3. 常用指令类型及其操作(重点)
3.1 传送指令
- 通用数据传送指令:
- MOV
- MOVS:符号扩展
- MOVZ:0扩展
- XCHG
- PUSH
- POP
- 地址传送指令:
- LEA(Load Effect Address):将源操作数的存储地址送到目的寄存器中
- 标志传送指令:
- pushf:将标志寄存器的内容压栈
- popf:将栈顶内容送到标志寄存器
3.2 定点算术运算指令
减法指令:后一个数减前一个数
比较指令
类似使用SUB指令,置标志位后再接条件转移指令或者条件设置指令。
乘法指令
- 只给出SRC,则另一个源操作数隐含在累加器AL/AX/EAX中,结果存放在2n位。
- 给出DST和SRC,结果放在n位DST中。
- 给出REG,SRC,IMM,则将SRC和立即数IMM相乘,结果存放在n位REG中。
关于乘法指令标志位的设置:
对于MUL指令:
若高位全为0,则OF和CF皆为0,否则皆为1
对于IMUL指令:
若高位全为0或者1,并且等于低位中的最高位,则OF和CF全为0,否则全为1
除法指令
指令中只会明显指出除数,用累加器AL/AX/EAX指出被除数
以8位除数为例,被除数被储存在AX寄存器中,商送回AL,余数在AH中(商在低位余在高)。
3.3 按位运算指令
逻辑运算指令
仅NOT指令不影响标志位,其他指令执行后,OF=CF=0,ZF和SF根据结果判断
- NOT
- AND
- OR
- XOR
- TEST:根据两个操作数相与的结果来设置条件标志,常用于需检测某种条件但是不能改变原操作数的场合。
移位指令
- SHL
- SHR
- SAL
- SAR
- ROL
- ROR
- RCL:带进位循环左移,将CF作为操作数的一部分循环左移
- RCR
3.4 控制转移指令
直接转移:转移的目标地址作为立即数直接出现在指令的机器码中
间接转移:转移的目标地址间接存储在某一寄存器或存储单元中
在IA-32指令系统中,所有段内直接转移都是相对转移,所有段内间接转移或者段间转移都是绝对转移。
条件转移指令
对于带符号数可以这样判断:根据SF直接得出结果符号,在根据OF判断结果是否正确,若结果不正确则实际的大小判断要反过来
例如,SF=1表示a-b结果是负的,本来应该是a<b,但是OF=1表示结果是错的,所以实际a>b。
条件设置指令
将条件标志组合得到的条件值设置到一个8位通用寄存器中,其设置的条件值与上表中的转移条件值完全一样。
调用和返回指令
- CALL DST:返回地址RA入栈,转到DST处执行
- RET:从栈中取出返回地址RA,转到RA处执行
3.5 x87浮点处理指令
IA-32有两种浮点处理架构:浮点协处理器架构、MMX发展而来的SSE指令集架构
x87FPU有一个浮点寄存器栈,深度为8,每个浮点寄存器有80位。
浮点数装入指令(FLD)float load:将存储单元中的浮点数装入到浮点寄存器的栈顶ST(0),无论是flds(float装入)还是fldl(double装入)都要先转换为80位扩展精度格式后再装入栈顶ST(0)。
FST(float store):浮点数存储指令,将浮点数装入到浮点寄存器栈的栈顶ST(0)。
FSTP:同上,但是会将该数据弹出栈
由于浮点寄存器的宽度为80位,因此需要先将80位扩展精度格式转换为32位或64位后,再存储到指定存储单元中,这可能会造成精度丢失。
3.6 MMX/SSE指令集
一个处理浮点数的指令集,提高了多媒体、图形图像等方面的处理能力。
4. C语言的机器级表示
4.1 过程调用的机器级表示
IA-32中用于过程调用的指令:CALL和RET
为了支持嵌套和递归调用,通常利用栈来保存返回地址,入口参数和过程内部定义的非静态局部变量。
CALL在跳转之前会将返回地址压栈,RET在返回之前会从栈中取出返回地址。
过程调用执行步骤
令P为调用者(caller),Q为被调用者(callee)。
- P将入口参数(实参)放到Q能访问到的地方。
- P将返回地址存到特定地方,然后将控制转移到Q。
- Q保存P的现场,并为自己的非静态局部变量分配空间。(准备阶段)
- 执行Q的过程体(函数体)。
- Q恢复P的现场,并释放局部变量所占用的空间。(结束阶段)
- Q取出返回地址,将控制转移到P。
原因:调用者和被调用者公用一套通用寄存器,因此被调用者不能破坏原有寄存器的内容,需要先保存到栈中。
现场:通用寄存器中原先的值
IA-32的寄存器使用规定
调用者保存寄存器:EAX,EDX,ECX(ADC),P在转到Q之前先保存他们的值,并在Q返回后先回复他们的值再使用。Q能够直接使用他们。
被调用者保存寄存器:EBX,ESI,EDI,Q必须在使用他们之前先将他们的值保存到栈中,并在返回P之前恢复它们的值。
IA-32的栈,栈帧及其结构
需要压入栈中的内容:过程的入口参数,返回地址,被保存寄存器的值,被调用过程中的非静态局部变量。
栈帧:为每个过程分配的栈区,一个栈由若干个栈帧组成。每个栈帧从EBP的旧值开始,到返回地址结束
帧指针寄存器:存放每个栈帧的起始位置,当前栈帧的范围在EBP和ESP指向的内存地址之间。在过程执行时,栈指针会不断移动,而帧指针可以固定不变。
注意:参数是倒着放的!
ESP+8总是指向第一个参数,之后的参数在此基础上每个+4。
变量的作用域和生存周期
非静态局部变量只在Q执行过程中有效,当Q返回P后,这些变量的内存都会被释放。它们的生存周期也只在该栈帧被分配的时间内。
C语言的外部参照型变量和静态变量被分配在静态数据区,具有“全局生存期”。
例:
该函数在执行过程中栈帧的使用情况如下:
leave指令功能:
movl %ebp, %esp
popl %ebp
将esp指向ebp的旧值,弹出ebp,即栈帧回到上一个函数,此时esp指向返回地址,通过ret返回到上一个函数的代码段。
递归过程调用
求自然数之和的递归函数例子:
递归的过程如图所示:
递归调用的缺点:
对于栈的开销很大,容易发生栈溢出。
非静态局部变量的存储分配
对于非静态局部变量的分配顺序,C标准规范中没有规定必须是按顺序从大地址到小地址还是反过来,因此属于未定义行为。
对不同变量的地址进行除==和!=之外的关系运算属于未定义行为,如if(&var1 < &var2)
会报错。
4.2 选择语句的机器级表示
if-else语句的机器级表示
大多结合cmp, jmp
和标号来实现
switch语句的机器级表示
switch可以直接跳转到某个条件处的语句执行,而不需要一一测试条件。
但是,当case的条件值相差较大时,编译器依然会生成分段跳转代码
循环结构的机器级表示
略
5. 复杂数据类型的分配和访问
5.1 数组的分配和访问
数组是数据集合,因而一定被放置在存储器中。
注意:指针类型的大小与操作系统的位数有关
数组元素和指针变量的表达式计算示例
5.2 结构体数据的分配和访问
指向结构体的指针就是其第一个字节的地址
结构体变量通常采用按地址传递的方式
5.3 联合体数据的分配和访问
联合体各个成员共享储存空间,分配给它的存储空间总是按照最大数据长度成员所需的空间大小为目标
利用联合体数据结构,可以实现对相同序列进行不同数据类型的解释
5.4 数据的对齐
在8字节宽的存储器机制下,访问数据的效率较高,因此需要进行数据的对齐。
i386 System V ABI 中对齐策略规定:
short地址是2的倍数
int, float, double和指针类型地址都是4的倍数
ABI规范只定义了变量的对齐方式,而没有定义变量的分配顺序,编译器可以自由决定使用何种顺序来分配变量。
i386 System V ABI对结构体有如下对齐方式要求:
- 整个结构体的对齐方式与最严格的成员相同
- 每个成员在满足其对齐方式的前提下,取最小的可用位置作为在结构体中的偏移量,可能内部插空
- 结构体大小为对齐边界长度的整数倍,可能尾部插空
6. 越界访问和缓冲区溢出
6.1 缓冲区溢出
在访问数组时发生超越数组存储区的越界访问,通常把这种存储区看成是一个缓冲区,这种超越数组存储区范围的访问称为缓冲区溢出。
6.2 缓冲区溢出攻击
将恶意代码段的首地址作为返回地址覆盖地写到原先正确的返回地址处,在ret指令执行后就会转到该段代码执行
防范方法:
用辅助攻击帮助查漏,如使用grep来搜索源代码中容易产生漏洞的库函数,或用fault injection差错
地址空间随机化ASLR:将加载程序时生成的代码段、静态数据段、堆区、动态库和栈区各部分的首地址进行随机化处理,使每次启动时,程序各段被加载到不同地址起始处
栈破坏检测:在函数准备阶段,在其栈帧中缓冲区底部与保存寄存器之间(如buffer[15]与保留的EBP之间)加入一个随机生成的特定值;在函数恢复阶段,在恢复寄存器并返回到调用函数前,先检查该值是否被改变。若改变则程序异常中止。因为插入在栈帧中的特定值是随机生成的,所以攻击者很难猜测出它是什么
可执行代码区域的限制:通过将程序栈区和堆区设置为不可执行,从而使得攻击者不可能执行被植入在输入缓冲区的代码,这种技术也被称为非执行的缓冲区技术
7. 兼容IA-32的64位系统
7.1 x86-64的基本特点
更多的通用寄存器个数
增加了8个64位寄存器R8~R15,可以作为8位寄存器(R8B ~ R15B),16位寄存器(R8W ~ R15W),32位寄存器(R8D ~ R15D)。
比IA-32具有更长的通用寄存器位数
所有通用寄存器被扩展到了64位,RAX,RBX,RCX,RDX, RBP,RSP,RSI,RDI,并且EBP,ESP,ESI,EDI的低8位可以使用,分别为BPL,SPL,SIL,DIL(注意是3个字母,区别AL,BL等)
字长从32位变成64位,逻辑地址相应变化
对于long double型数据,虽然还是采用IA-32相同的80为扩展精度格式,但分配的存储空间从12字节扩展为16字节,从4B对齐改为16B对齐,但使用时都只会用到低10字节
过程调用时,如果入口参数只有6个以内的整型变量和指针型变量,通常采用通用寄存器而不是栈来传递。
128位的XMM寄存器从8个增加到16个,浮点操作采用基于SSE的面向XMM的指令集
7.2 x86-64的基本指令和对齐
当指令中的操作数为存储器操作数时,其基址寄存器或者变址寄存器必须是64位寄存器。(因为地址是64位的)
数据传送指令
movabsq
:将64位立即数(abs)送到64位通用寄存器中
movq
:传送一个64位的四字
movsbq, movswq, movslq
:符号扩展
movzbq, movzwq, movzlq
:零扩展
leaq
:将有效地址加载到64位寄存器
pushq, popq
:压栈,出栈
movl
:在传送32位寄存器内容的同时,将目的寄存器的高32位自动清零,也因此movl
相当于movzlq
指令
算术逻辑运算指令
addq, subq, imulq(带符号整数相乘), mulq, orq, incq, decq, negq, notq, salq
数据对齐
任何K字节宽的基本类型数据和指针类型数据的起始地址一定是K的倍数
long, double型数据和指针变量都必须按8字节对齐;long double型数据必须按16字节边界对齐
7.3 x86-64的过程调用
可以不用栈指针寄存器RBP作为栈帧底部,而是使用RSP作为基址寄存器来访问栈帧中的信息
传送入口参数的寄存器依次为RDI,RSI,RDX,RCX,R8,R9(6个),返回参数放在RAX中
调用者保存寄存器为R10,R11,被调用者保存寄存器为RBX,RBPR12,R13,R14,R15
RSP用于指向栈顶元素
RIP用于指向正在执行或者即将执行的命令
7.4 x86-64的浮点数操作与SIMD指令
浮点运算采用基于SSE的面向XMM寄存器的SIMD指令,浮点数放在128位的XMM寄存器中,不再放在寄存器栈中。
第四章 程序的链接
1. 编译、汇编和链接
1.1 编译和汇编
预处理:对头文件的包含,宏定义的扩展,条件编译的选择,“gcc -E main.c -o main.i”或“cpp main.c -o main.i”
编译:对源程序进行词法分析、语法分析和语义分析,并进行优化和存储分配,最终把C语言源程序翻译成汇编语言程序。对应指令为“gcc -S main.i -o main.s”
汇编:将编译生成的汇编语言代码转换为机器语言代码,不能确定每条指令或者每个数据最终的地址,需要进行重定位。对应指令为“gcc -c main.c -o main.o”
1.2 可执行目标文件的生成
将所有关联的可重定位目标文件组合起来,以生成一个可执行文件。”ld -o test main.o test.o”,ld是静态链接指令
可重定位目标文件的代码总是从0开始,而可执行文件的代码在虚拟地址空间产生
可重定位目标文件合成可执行目标文件需要以下两步:
符号解析
将每个符号的引用与一个确定的符号定义相关联。符号包括全局静态变量名和函数名。非静态局部变量不是符号。
编译器会将所有符号存放在可重定位目标文件的符号表中。
重定位
代码区和数据区都是从0地址开始的。链接器需要将不同模块中相同的节合并起来生成一个单独的节,并进行虚拟地址空间划分来重新确定位置。重新确定代码和数据的地址并更新指令中被引用符地址号叫做重定位。
链接的好处:实现“模块化”,方便编写,建立函数库;每个模块分开编译,提高开发效率。
2. 目标文件格式
目标代码:将编译器或汇编器处理源代码后生成的机器语言目标代码
目标文件:存放目标代码的文件
2.1 ELF目标文件格式
目标文件包含机器代码、数据、重定位信息和调试信息。
ELF(Executable and Linkable Format):可执行可链接格式,由UNIX操作系统使用
节:ELF文件中具有相同特征的最小可处理信息单位
代码节(.text)、只读数据节(.rodata)、已初始化全局数据节(.data)、未初始化全局数据节(.bss)。
段:描述目标文件中的节如何映射到存储空间的段中,可以将多个节合并后映射到同一个段,如.data和.bss映射到可读可写数据段中。
节头表:包含文件中各个节的说明信息。有每个节的名字和大小之类的信息。可重定位目标文件一定要有节头表。
程序头表:指示系统如何创建进程的存储器映像。可执行文件和共享库文件必须要有程序头表。
2.2 可重定位目标文件格式
总体结构如下:
ELF头
位于目标文件的起始位置,包含文件结构的说明信息。共52个字节,仅ELF头在文件中具有固定的位置,其他部分由ELF头和节头表指出。
e_shoff指出节头表在文件中的偏移量
使用”readelf -h main.o”对文件进行解析
节
ELF文件中的主体信息,包含了链接过程中所使用的目标代码信息,包括指令,数据,符号表和重定位信息等。
.text:目标代码
.rodata:只读数据,如printf中的格式串,开关语句(switch-case)的跳转表
.data:已初始化全局变量
.bss:未初始化全局变量。无须在目标文件中分配用于保存值的空间,仅仅是一个占位符
.symlab:符号表,程序中定义的函数和全局静态变量名都是符号
.rel.text:.text节相关的可重定位信息。当目标文件组合时,指令中的引用操作数地址信息或者跳转目标指令位置信息都要被修改。一般调用外部函数或者引用全局变量的指令中的地址字段需要被修改。
.rel.data:.data节相关可重定位信息。
.debug:调试用符号表。
.line:行号和.text节之间的映射。
.strlab:字符串表,包括.symlab节和.debug节中的符号以及节头表中的节名
节头表
由若干个表项组成,每个表描述相应的一个节的节名,在文件中的偏移,大小,访问属性,对齐方式等。
使用
readelf -S test.o
来解析文件得到如下结果将该信息映射到文件结构中:
2.3 可执行目标文件格式
ELF可执行目标文件由ELF头、程序头表、节头表和不同的节组成。
程序头表:将可执行文件中连续的、具有相同访问属性的代码和数据段映射到存储空间(通常是虚拟地址空间)中,程序头表用于描述这种映射关系。
32位程序头表具有以下数据结构:
使用readelf -l filename
指令显示可执行文件的程序头表信息:
2.4 可执行文件的存储映像
可执行文件到虚拟地址空间的映射是建立存储映像的过程。
i386 System V ABI规定,只读代码段总是映射到从虚拟地址0x8048000开始的一段区域,可读写数据段映射到只读代码段后面按4KB对齐的高地址上,运行时堆则在可读写数据段后面4KB对齐的高地址处,运行时用户栈则是从用户空间的最大地址往低地址方向生长。
3. 符号表和符号解析
3.1 符号和符号表
全局符号:非静态函数名和被定义为不带static属性的全局变量名
外部符号:在其他模块定义的外部函数名和外部变量名
本地符号:带static属性的函数名和全局变量名
ELF文件中包含的符号表中每个表项具有以下数据结构:
另外,st_other显示符号可见性,st_shndx指出符号所在节在节头表中的索引。
可通过readelf -s main.o
查看符号表:
3.2 符号解析
全局符号的强弱特性:
强符号:函数名和已初始化的全局变量名
弱符号:未初始化的全局变量名
链接器处理符号的规则如下:
- 强符号不能多次定义。
- 强符号覆盖同名弱符号。
- 多个弱符号任选一个。
符号解析过程:
链接器在进行符号解析时需要维护3个集合:
集合E指被合并到一起的组成可执行文件的所有目标文件集合
集合U是未解析符号集合
集合D是当前为止已经加入到E的所有目标文件中定义符号的集合
- 对每个输入文件f,若为目标文件,将f加入到E,并修改U,D
- 若为库文件,则匹配U中符号和f中各目标模块定义的符号
- 若往D中加入一个已存在的符号,或者扫描完所有输入文件时U非空,则链接器报错
3.3 与静态库的链接
4. 重定位
重定位的目的是在符号解析的基础上将所有关联的目标模块合并,并确定运行时每个定义符号在虚拟空间中的地址,重定位引用的地址。
- 节和定义符号的重定位。合并所有节
- 引用符号的重定位。其中重定位信息放在.rel.text和rel.data中,告知链接器目标文件中哪些引用符号需要重定位,所引用的是哪个定义符号等
重定位信息
表项数据结构如下:
r_offset指出需重定位的位置相对节起始位置偏移量。
r_info包含符号索引和重定位类型,其中r_sym指出符号在符号表中的位置,r_type分为PC(下条指令地址)相对寻址和绝对地址。
重定位表信息可通过readelf -r main.o
来显示。
IA-32 中转移目标地址(即有效地址)计算公式为:转移目标地址 =PC+ 偏移地址
PC相对地址方式下的重定位值计算公式为:ADDR(r_sym) - ((ADDR(.text) + r_offset) - init)
绝对地址可直接加算
5. 动态链接
特性:
- 共享性:代码段在内存中只存在一份副本,不需要将代码合并到生成文件中去
- 动态性:只在使用它的程序被加载或者执行时才加载到内存,因而在共享库更新后并不需要重新对程序进行链接
生成指令:gcc -shared -fPIC -o mylib.so myproc1.c myproc2.c
-fPIC(Position-Independent Code): 位置无关代码,即共享库代码与位置无关。
5.1 程序加载时的动态链接
生成可执行文件指令:gcc -o myproc main.c ./mylib.so
5.2 程序运行时的动态链接
指令:gcc -rdynamic -o myproc main.c -ldl
1 |
|