1 信息的表示和处理
1.1 信息存储(略)
1.2 整数表示
1.2.1 整数数据类型
1.2.2 无符号数的编码
1.2.3 补码编码
1.2.4 有符号数和无符号数的转换
1.2.5 C语言中的有符号数和无符号数
1.2.6 扩展一个数的位表示
无符号数在前面补0,称为零扩展
补码数在前面补1,称为符号扩展
1.2.7 截断数字
利用取模运算(取余),将后面的位取下来。
可能会改变原本的值,例如将一个32bit数,截取16位。如果无符号数>2^16,或者补码数>2^15(有一个要当符号位),就会改变值,向下溢出。
1.2.8 关于有符号数与无符号数的建议
隐式强制类型转换可能会导致bug,需求,定义,使用用相同的数据类型。
1.3 整数运算
1.3.1 无符号加法
当数字相加大于可表示范围,则会发生溢出。
判断是否溢出:X+Y >= X,则没有发生溢出。
w bit无符号数x求反:除了0的反数是0 ,其他都是(2^w) – x
1.3.2 补码加法
X+Y>2^(w-1):正溢出 🡪 X+Y=X+Y-2^(w-1)
X+Y<2^(w-1):负溢出 🡪 X+Y=X+Y+2^(w-1) (负数相加)
判断是否溢出:
当x>0,y>0; x+y<=0 正溢出
当x<0,y<0; x+y>=0 负溢出
1.3.3 补码的非
逆元即相加等于零的值
补码的最小值min的逆元是min, 其他数x的值的逆元是-x
例子: -128 = 1000 0000 1000 0000 + 1000 0000 = (溢出)1 0000 0000
1.3.4 无符号乘法
两个w位的值相乘会得到2w位的值, 但C语言里面只会取后w位的值.
X Y = (X Y)Mod 2^w
1.3.5 补码乘法
1.3.6 乘以常数
处理器处理乘法需要多个时间周期,编译器会试图用位移,加法,减法组合来消除乘法.
比如 X * 14 = (X<<3) + (X<<2)+ (X<<1) 14 = 8 + 4 + 2
也可以化为 (X<<4) – (X<<1) 14 = 16 - 2
1.3.7 除以2的幂
处理器处理除法会比乘法还慢.
整数在进行除法时,如果出现小数,可以向下舍入(正数),或向上舍入.(负数)
例子: 3.14 -3.14
向下舍入 3 -4
向上舍入 4 -3
除以2的幂时:
在无符号数里,右移,前补0,就可以得到向下舍入的值
有补码数里,右移,前补1,得到向上舍入的值
1.3.8 关于整数运算的最后思考
1.4 浮点数
1.4.1 二进制小数
对比十进制:
123.4 = 100 + 20 + 3 +0.4
那么二进制
110.1 = 4 + 2 + 0 + 0.25
在有限的长度下,无法表示 1/3 1/7 这一类的数
1.4.2 IEEE浮点表示
V = (-1)^S M 2^E
存储格式(float):
32 31-23 22-0
s e f
S 是符号,用来表示正负,在C语言浮点类型中占一个bit
E 是阶码,float中占8 bit ; double中占11 bit;
M是尾数,float中占23bit; double中占52位
可能的情况:
1.阶码E != 0 & E != 255 规格化的数,M = 1 + f ; E = (e – 127)
2.阶码E = 0 非规格化的数,M = f; E = 1 – (e – 127)
3.阶码E = 255 ; 位数 M = 0 无穷大
位数 M != 0 NaN
1.4.3 数字示例
1.4.4 舍入
因为表示方式限制了浮点数的范围和精度,所以浮点运算只能近似的表示浮点运算.经常需要用到舍入,来找到最接近的值.
1.5 2.5 -1.5
向下舍入 1.0 2.0 -2
向上舍入 2.0 3.0 -1
向零舍入 1.0 2.0 -1 (去掉小数部分)
向偶数舍入 2.0 2.0 -2 (最近的数,有2个,则取最近的偶数)
向偶数舍入是默认舍入方式,因为其向下舍入和向上舍入的情况各50%
1.4.5 浮点运算
因为舍入,不具备结合性: 3.14+ln10-ln10 = 0; 3.14+(ln10-ln10) = 3.14
但浮点运算却满足单调性,如果a>b,则x+a>x+b, 整型数则不一定
1.4.6 C语言中的浮点数
C语言不要求使用IEEE浮点,没有规定舍入方式和特殊值的标准;
许多细节随系统和编译器的不同而不同.
#define _GNU_SOURCE 1
#include
在GCC中,上面两行语句定义了常数 INFINITY 表示正无穷,NAN表示NaN
2 程序的机器级表示
2.1 历史观点
2.2 程序编码
2.2.1 机器级代码
两种抽象:
- 将程序行为描述成好像每条指令都是顺序执行的
- 机器级程序使用的内存地址是虚拟内存地址
常用处理器状态
- 程序寄存器(PC):指向下一条指令在内存中的地址,x86-64中是%rip
- 整数寄存器文件包含16个命名空间的位置,分别存储64位的值;可以存储一些地址或者整数的数据。有的用来记录某些重要的程序状态,有的则用来保存临时数据。
- 条件码寄存器:保存最近执行的算数或逻辑指令的状态信息,它们用来实现控制或数据流中的条件变化,比如用来实现 if 和 while 语句。
- 一组向量寄存器可以存放一个或多个整数或浮点数值。
程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的内存块。
2.2.2 代码示例
gcc -Og -S xxx.c // -Og优化 -O1 -O2 ; -S产生汇编文件
gcc -Og -c xxx.c // -c 产生目标代码文件
objdump -d xxx.o // -反汇编
x86-64的指令长度从1~15个字节不等。常用指令字节少,不常用指令多。
生成可执行的代码需要对一组目标代码文件运行链接器,需要main函数。
gcc -Og -o prog xxx.c yyy.c //进行链接,生成可执行文件
链接后变化,链接上了一些标准库代码.代码地址有重新分配.代入了调用函数需要用到的地址.当函数代码没满16字节时,会补上nop填充,方便管理.
2.2.3 关于格式的注解
以 “ . ”开头的行一般是指导汇编器的链接器工作的伪指令.
C语言中插入汇编代码方法:
- 用汇编语言写整个的函数,在链接阶段合并.
- 利用GCC的支持,嵌入汇编代码.
2.3 数据格式
byte 字节 8bit char b
word 字 16 bit short w
double words 双字 32 bit int l float : s
quad words 四字 64 bit char * / long q double: l
Intel历史上还有一种10字节的long double类型.不建议使用.
数据传送指令: movb / movw / movl / movq
2.4 访问信息
X86-64 CPU 含有16个64bit通用目的寄存器,用来储存整数数据和指针.
因为向上兼容的原因,指令可以字节对低位字节进行操作.
生成小于8字节数字时的两条规则:
- 生成1字节,2字节数字的指令会保持剩下的字节不变
-
2.4.1 操作数指示符
大多数指令有一个或多个操作数,指示出操作需要的原数据和放置结果的目的地址。
直接以常数形式给出的叫立即数。 $0x1F
- 寄存器的内容。 R[ra]
- 内存引用,根据计算出的地址,访问内存地址取值.
MOVZ指令包括:movzbw,movzbl,movzwl,movzbq,movzwq 进行零扩展
MOVS指令: movsbw,movsbl,movswl,movsbq,movswq,movslq 符号扩展
cltq 把%eax 符号扩展到 %rax
总结:符号扩展传送每个都有,零扩展只有字节和字有.
2.4.3 数据传送示例
2.4.4 压入和弹出数据
pushq S 将四字压入栈 相当于栈指针-8,值写进新的栈顶地址
popq D 将四字弹出栈
%rsp 保存着栈顶元素的地址
根据惯例,栈是倒着存储的,所以才会有压栈-8弹栈+8
2.5 算数和逻辑操作
2.5.1 加载有效地址
- leaq S,D &S🡪D
- leaq也可以简介的描述普通的算数操作
leaq (%rdi,%rsi,4),%rax (%rsi的值 * 4)+%rdi的值🡪%rax
2.5.2 一元和二元操作
一元操作只有一个操作数,该操作数既是源又是目的.
INC D D++
DEC D D—
NEG D -D 取反
NOT D ~D 取补
二元操作
ADD S,D D=D+S
SUB是减
IMUL是乘
XOR异或
OR或
AND与
2.5.3 移位操作
SAL k,D D=D<
SAR算术右移 填上符号位 适合补码数
SHR逻辑右移 填上0 适合无符号数
2.5.4 讨论(略)
2.5.5 特殊的算术操作
128bit 八字 oct word
imulq S S * R[%rax] 🡪 R[%rdx] : R[%rax] 无符号的用mulq
cqto 将%rax的值符号扩展后🡪 R[%rdx] : R[%rax]
idivq S R[%rdx] : R[%rax] % S🡪 R[%rdx]
R[%rdx] : R[%rax] / S🡪 R[%rdx] 无符号的用divq
2.6 控制
2.6.1 条件码
- CF : 进位标志,最近操作使最高位产生了进位,可用来判断无符号溢出
- ZF : 零标志,最近操作结果为0
- SF ; 符号标志,最近操作结果负数
- OF : 溢出标准,最近操作导致补码溢出
CMP指令根据两个操作数之差设置条件码,类似SUB
CMP(cmpb,cmpw,cmpl,cmpq) S1,S2 比较S2-S1
2.6.2 访问条件码
条件码通常不会直接读取,常用使用方法:
- 根据条件码某种组合,将一个字节设置为0/1; SET指令(P137)
- 条件跳转
- 条件传送数据
2.6.3 跳转指令
jmp 无条件跳转,可以跳到标签处,也可以跳到某地址
jmp %rax %rax的值是目标
jmp (%rax) %rax的值指向的内存空间的值是目标
jump指令详细看P1392.6.4 跳转指令的编码
在链接时,一般是根据和目标的相对地址进行编码。也可以给出绝对地址2.6.5 用条件控制来实现条件分支
if(条件){
goto A;
}
代码2…
A:
代码1…
cmpq %rsi , %rdi
jge .L2
代码2…
.L2:
代码1…
if(条件){
代码1…
}else{
代码2…
}
2.6.6 用条件传送来实现条件分支
if(x>y){
z = x + y;
}else{
z = y – x;
}
return z;
z1 = x + y;
z2 = x – y;
if(x>y){
return z1;
}else{
return z2;
}
movq …(伪汇编
%rax =%rsi + %rdi;
%rdx = %rsi - %rdi;
)
cmpq %rsi, %rdi
cmovge %rdx, %rax
ret
CPU采用流水线获得高性能,遇到条件跳转指令时,会采用分支预测逻辑来猜测.有几率猜错,浪费时钟周期,因此条件传送实现条件分支性能会更高
2.6.7 循环
do-while
loop:
//语句
if(t) goto loop;
while
第一种
goto test:
loop:
//语句
test:
if(t) goto loop;
第二种
if( ! t ) goto done:
do
//语句
while( t);
done:
for
等价于while变形
2.6.8 switch
switch(i){
case 1: //语句;
case 2: //语句;
case 3,4,5…
}
🡪
static void jt[5] = {&&loc_1,&&loc_2,…}
goto jt[i];
loc_1:
//语句
loc_2:
//语句
loc_3,4,5..
done:
//break;则跳转到这里
2.7 过程
2.7.1 运行时栈
当过程需要的储存空间大于寄存器能够储存的大小时,就会在栈上分配空间.
较早的帧🡪参数🡪返回地址🡪被保存的寄存器值🡪局部变量🡪参数构造区🡪栈顶(可能变长)
2.7.2 转移控制
函数P调用call🡪函数Q:将PC指向Q,将P函数中下一指令地址压入栈中
2.7.3 数据传送
过程可能需要传送参数,返回值,寄存器只能传送6个参数.超过六个则使用栈
2.7.4 栈上的局部存储
使用情况:
- 寄存器不够存放所有本地数据
- 对一个局部变量使用”&”,必须产生一个地址
-
2.7.5 寄存器上的局部存储空间
寄存器组是过程中唯一被所有过程共享的资源.
%rbx,%rbp和%r12~%r15 : 被调用者保存寄存器
所有其他寄存器,除了%rsp 都是 调用者保存寄存器2.7.6 递归过程
2.8 数组分配和访问
3
4
5
6 链接
6.1 编译器驱动程序
6.2 静态链接
符号解析:将符号引用和符号定义关联起来
重定位:把符号定义和内存空间关联起来,然后重定向。6.3 目标文件
三种形式:
可重定向目标文件,编译后未链接的文件, windows的.obj文件,linux的.o文件
- 可执行目标文件,链接后的文件, windows 下的.exe文件,linux下/bin/bash文件
- 共享目标文件,可以被动态的加载进内存并链接, windows下的DLL文件,linux下的.so文件
- (书里没写)核心转储文件:当进程意外终止时,系统用来储存该进程地址空间的内容以及终止时的一些其他信息, linxu下的core dump
目标模块:一个字节序列
目标文件:以文件形式保存的目标模块
Unix用a.out格式,Windows里用PE格式,Mac OS X用Mach-O格式,现代Unix/Linux用ELF格式
6.4 可重定位目标文件
以ELF格式为例子:
ELF头:一个16直接序列(描述生成该文件的系统的字的大小和字节顺序),帮助链接器语法分析和解释目标文件的信息(ELF头大小,目标文件的类型,机器类型,节头部表的文件偏移,节头部表中条目的大小和数量)
.text:已编译程序的机器代码
.rodata:只读数据,比如printf的格式串和开关语句的跳转表
.data:已初始化的全局变量和静态C变量.局部变量运行时保存在栈中.
.bss:未初始化/初始化为0 的全局变量和静态C变量.在目标文件中不占据实际空间,仅仅是一个占位符,符号保存在.symtab中,节头部首记录了大小;.bss只是为了空间效率(运行时,在内存中分配这些变量,初始值为0)
.symtab:一个符号表,存放在程序中定义和引用的函数和全局变量的的信息.
.rel.text:一个.text节点中位置的列表,当多个目标文件链接一起时,需要修改这个列表.
.rel.data:被模块引用或定义的所有全局变量的重定位信息.
.debug:一个调试符号表,编译时加-g会得到这张表.
.line:原始C源程序的行号的.text节中机器指令之间的映射.
.strtab:一个字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字.
节点部表:描述不同节的位置和大小
6.5 符号和符号表
每个可重定位目标模块m都有一个符号表,在链接器的上下文中,有三种不同的符号:
- 全局链接器符号:由模块m定义,被其他模块引用的全局符号.对应非静态的C函数和全局变量.
- 外部符号:由其他模块定义,被模块m引用的全局符号,对应其他模块的非静态的C函数和全局变量.
- 只被模块m定义和引用的局部符号,.对应静态static的C函数和全局变量.只能在当前模块内使用,
有趣的是:C语言static变量不是在栈上分配的,而是在.data或.bss中定义分配,而且还在符号表中创建一个有唯一符号的本地链接器符号.所以C语言可以在不同函数中创建多个static类型的同名变量而不会冲突.