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 补码乘法

和无符号乘法一样取后w位,都有可能出现溢出问题

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 数字示例

举栗子,省略,详细看P80

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 历史观点

讲了Intel,AMD,x86的发展历史

2.2 程序编码

2.2.1 机器级代码

两种抽象:

  1. 将程序行为描述成好像每条指令都是顺序执行的
  2. 机器级程序使用的内存地址是虚拟内存地址

常用处理器状态

  1. 程序寄存器(PC):指向下一条指令在内存中的地址,x86-64中是%rip
  2. 整数寄存器文件包含16个命名空间的位置,分别存储64位的值;可以存储一些地址或者整数的数据。有的用来记录某些重要的程序状态,有的则用来保存临时数据。
  3. 条件码寄存器:保存最近执行的算数或逻辑指令的状态信息,它们用来实现控制或数据流中的条件变化,比如用来实现 if 和 while 语句。
  4. 一组向量寄存器可以存放一个或多个整数或浮点数值。

程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的内存块。

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语言中插入汇编代码方法:

  1. 用汇编语言写整个的函数,在链接阶段合并.
  2. 利用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. 生成1字节,2字节数字的指令会保持剩下的字节不变
  2. 生成4字节的数字的指令会把高位4个字节置为0

    2.4.1 操作数指示符

    大多数指令有一个或多个操作数,指示出操作需要的原数据和放置结果的目的地址。

  3. 直接以常数形式给出的叫立即数。 $0x1F

  4. 寄存器的内容。 R[ra]
  5. 内存引用,根据计算出的地址,访问内存地址取值.
    1. 绝对寻址 M[Imm]
    2. 间接寻址 M[R[ra]]
    3. (基址+偏移量)寻址 M[Imm + R[ra]]
    4. 变址寻址 M[R[ra1] + R[ra2]]
    5. 变址寻址 M[Imm+ R[ra2] * s]

      2.4.2 数据传送指令

      movb / movw / movl / movq S,D S🡪D
      movq只能以32bit补码立即数位源操作数,扩展成64位进行传送
      movabsq I,R I🡪R 任意64bit立即数
      不能从内存直接传送到内存

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 加载有效地址

  1. leaq S,D &S🡪D
  2. 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<SHL同上
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 条件码

  1. CF : 进位标志,最近操作使最高位产生了进位,可用来判断无符号溢出
  2. ZF : 零标志,最近操作结果为0
  3. SF ; 符号标志,最近操作结果负数
  4. OF : 溢出标准,最近操作导致补码溢出

CMP指令根据两个操作数之差设置条件码,类似SUB
CMP(cmpb,cmpw,cmpl,cmpq) S1,S2 比较S2-S1

TEST指令类似于AND,判断S1&S2

2.6.2 访问条件码

条件码通常不会直接读取,常用使用方法:

  1. 根据条件码某种组合,将一个字节设置为0/1; SET指令(P137)
  2. 条件跳转
  3. 条件传送数据

    2.6.3 跳转指令

    jmp 无条件跳转,可以跳到标签处,也可以跳到某地址
    jmp %rax %rax的值是目标
    jmp
    (%rax) %rax的值指向的内存空间的值是目标
    jump指令详细看P139

    2.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 栈上的局部存储

使用情况:

  1. 寄存器不够存放所有本地数据
  2. 对一个局部变量使用”&”,必须产生一个地址
  3. 数组或结构

    2.7.5 寄存器上的局部存储空间

    寄存器组是过程中唯一被所有过程共享的资源.
    %rbx,%rbp和%r12~%r15 : 被调用者保存寄存器
    所有其他寄存器,除了%rsp 都是 调用者保存寄存器

    2.7.6 递归过程

    2.8 数组分配和访问

    3

    4

    5

    6 链接

    6.1 编译器驱动程序

    语言预处理器,编译器,汇编器,链接器

    6.2 静态链接

    符号解析:将符号引用和符号定义关联起来
    重定位:把符号定义和内存空间关联起来,然后重定向。

    6.3 目标文件

    三种形式:

  4. 可重定向目标文件,编译后未链接的文件, windows的.obj文件,linux的.o文件

  5. 可执行目标文件,链接后的文件, windows 下的.exe文件,linux下/bin/bash文件
  6. 共享目标文件,可以被动态的加载进内存并链接, windows下的DLL文件,linux下的.so文件
  7. (书里没写)核心转储文件:当进程意外终止时,系统用来储存该进程地址空间的内容以及终止时的一些其他信息, 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都有一个符号表,在链接器的上下文中,有三种不同的符号:

  1. 全局链接器符号:由模块m定义,被其他模块引用的全局符号.对应非静态的C函数和全局变量.
  2. 外部符号:由其他模块定义,被模块m引用的全局符号,对应其他模块的非静态的C函数和全局变量.
  3. 只被模块m定义和引用的局部符号,.对应静态static的C函数和全局变量.只能在当前模块内使用,

有趣的是:C语言static变量不是在栈上分配的,而是在.data或.bss中定义分配,而且还在符号表中创建一个有唯一符号的本地链接器符号.所以C语言可以在不同函数中创建多个static类型的同名变量而不会冲突.