数据存储
1b(bit)是一个二进制位,值为0/1
1B(byte)是一个字节,=8b
一般都至少以B为单位
单位 | n(210*n字节/单位) |
---|---|
KB | 1 |
MB | 2 |
GB | 3 |
TB | 4 |
不常用 | |
PB | 5 |
EB | 6 |
ZB | 7 |
YB | 8 |
进制(2/8/10/16)
用于数字的计算和表示,n进制表示满n进1
进制 | 用表示的字符集 | 识别前缀 |
---|---|---|
2 | [0-1] | |
8 | [0-7] | 0 |
10 | [0-9] | |
16 | [0-9],[a-f] (a-f分别表示10-15,字母大小写均可) | 0x,大小写均可 |
比如 0x12
表示16进制数12
10进制值为18
原码/反码/补码
以32位数举例
符号位:0位正,1位负
原码:第一位符号位,后面31位表示绝对值大小
反码:在原码基础上,如果是负数,后面31位取反
补码:在反码基础上,如果是负数,+1
一组相反数,两数相加会最终导致溢出(原来的32位全为0,最前面溢出一个1),最终结果(取32位)是0
一般的数全都用补码表示(在正数范围内。原码与反码一样),这样就很方便地将减法变成加法
一个正数变负数(x->-x),以32为为例,有3种方法,第一种 最方便理解;第二种 最快&最简单使用;第三种 最原始但慢
- 负数(-x)的机器数等于2^32-正数(x)
- x的机器数从右向左,找到第一个1,这个1左边的未全部取反
- 按照 原码->反码->补码 的顺序算
数的表示
有符号数均用补码表示
字符/指针
字符 16b有符号数
指针 无符号数(位数取决于机器是多少位)
整数
整数的大小用补码表示
类型 | 含义 |
---|---|
short | (有符号)短整数,16b |
unsigned int | 无符号短整数,16b |
int | 整数,32b |
unsigned int | 无符号整数,32b |
long long | 长整数,64b |
unsigned long long | 无符号长整数,64b |
浮点数(float/double)
使用IEEE754标准,
float32b,double64b
类型 | 符号/阶码/位数 |
---|---|
float | 1/8/21 |
double | 1/11/52 |
数= +/- 1.尾数*2^阶码
尾数为2进制无符号整数(可以通过10进制的0.xxx *2取整获得),最后有多余则进1
阶码为有符号整数
整数的输入与转换
C90/C99
数值范围 | C90 | C99 |
---|---|---|
0~2 ^ 31-1 | int | int |
2^31 ~ 2^32-1 | unsigned int | long long |
2^32 ~ 2^63-1 | long long | long long |
2^63 ~ 2^64-1 | unsigned long long | unsigned long long |
解析
先将数拿进来,不看前面的符号(即看它的绝对值)给一个类型,再看符号对机器数进行一定规则的转换(主要是 正变负)
按照不同的C语言标准
整数类型转换
位截断/位扩展(位数不同)
位截断:比如64位数转成32位数,就是很粗暴地把前面不需要的扔掉
位扩展:把32位数转成64位数,分为 0扩展 和 符号扩展
有符号数采用符号扩展:前面多出来的位补符号位
无符号数采用0扩展:前面多出来的位补0
位数相同,有无符号不同
直接将机器码copy过去
计算时的统一类型
尾数 短->长
符号 有->无
精度 小->大,(能表示的最小的整数 越小,精度越大)
比如 2147483647>-2147483648 (C90)的值为false
因为 2147483648被理解为unsigned int,机器数为(10…),
再看前面的负号,机器数改变后还和之前一样
而比较时,类型会统一为unsigned int,左边的那个数机器码不变,(
按无符号数比较)左边(01…)明显<右边(10…)
float与int都是32位,
float转int时会丢失精度(小数位丢失)
int能表示的最大的数比float大
浮点数运算
加减
对阶(小阶对大阶)
提出那个阶数比较大的那个
目的:使两个(每个)浮点数的较大的整数部分 为1
左归/右归
左归:数相对小数点向左移,次数-1
右归:数相对小数点向右移,次数+1
目的:保持整数位为1,方面再次表示为浮点数
乘除(较复杂,忽略)
指令集架构(ISA)
生成机器代码步骤
使用gcc,可以直接一步完成(gcc 文件名 -o 目标文件名)
也可以分步来(当不写命令选项时,直接将后续的步骤都做了)
命令格式:gcc -命令选项 文件名 -o 目标文件名
一开始的文件(.c)
第几步 -命令选项 目标文件格式 详细
- (-E -i)预处理:处理#开头的语句(#include,#define)
- (-S -s)编译:生成汇编语言程序
- (-c -o) 汇编:生成可重定位文件(二进制)
- (无命令选项 文件无后缀)连接(包括符号解析和重定位):生成可执行文件(二进制)
寄存器
寄存器 | 名称 | 大小/内部结构 |
---|---|---|
通用寄存器 | 32b | |
EAX | 累加器 | AX:低16b(AH:高8b;AL:低8b) |
EBX | 基址寄存器 | BX:低16b(BH:高8b;BL:低8b) |
ECX | 计数寄存器 | CX:低16b(CH:高8b;CL:低8b) |
EDX | 数据寄存器 | DX:低16b(DH:高8b;DL:低8b) |
ESP | (栈顶)栈指针寄存器 | SP:低16b |
EBP | (栈底)基址指针寄存器 | BP:低16b |
ESI | 源变址寄存器 | SI:低16b |
EDI | 目标变址寄存器 | DI:低16b |
专用寄存器 | 32b | |
EIP | 指令指针寄存器 | IP:低16b |
EFLAGS | 标志寄存器 | FLAGS:低16b |
段寄存器 | 16b | |
CS | 代码段 | |
SS | 堆栈段 | |
DS | 数据段 | |
ES | 附加段 | |
FS | 附加段 | |
GS | 附加段 |
寻址方式(地址表示)
寻址方式 | 机器指令格式 | 书面格式 |
---|---|---|
寄存器寻址 | %寄存器名 | R[寄存器名],如eax |
存储器寻址 | 偏移量(基址,变址,比例系数) | M[地址] |
立即数寻址 | $值 | 值 |
存储器寻址的地址计算:偏移量+基址+变址*比例系数
存储器寻址每一个值都可以省略(比例系数的默认值为1,其他的为0),但()内的如果后面的数省略了,逗号也可以相应地省略(如果把前面的省略了而后面的数不省略,需要把逗号补上)
标志位(EFLAGS)
常用的有
标志位 | 含义 | 判断标准 |
---|---|---|
常用 | ||
OF(overflow flag) | 溢出标志 | 左1进位 && 左2进位 |
SF(sign flag) | 符号标志 | =符号位 |
ZF(zero flag) | 零标志 | 运算结果是否为0 |
CF(carry flag) | 进/借 位标志 | 是否移除或(因不够减而)借位 |
控制标志(次常用) | ||
DF(firection flag) | 方向标志 | |
IF(interrupt flag) | 中断允许标志 | |
TF(trap flag) | 陷阱标志 |
OF用于判断有符号数是否溢出
CF用于判断无符号数是否溢出
指令
数据传送 | 地址数 | 功能 |
mov | 2 | 地址2的数=地址1的数 |
movs | 2 | 送数据同时符号扩展 |
movz | 2 | 送数据同时0扩展 |
xchg | 2 | 交换两个数 |
push | 1 | 将地址里的数入栈 |
pop | 1 | 出栈一个数到地址1里 |
地址传送指令 | ||
lea | 地址2的数=地址1 | |
输入输出 | ||
in | 1 | |
标志传送 | ||
pushf | 1 | |
popf | 1 | |
函数调用与返回 | ||
call | 1 | 函数的目标地址 |
leave | 0 | |
ret | 0 | 函数返回 |
算数运算 | ||
add | 2 | 地址2的数+=地址1的数 |
sub | 2 | 地址2的数-=地址1的数 |
cmp | 2 | 地址2的数-地址1的数,数不改变大小,常用于跳转的计算 |
inc | 1 | 地址1的数++ |
dec | 1 | 地址1的数– |
neg | 1 | 地址1的数=-地址1的数 |
mul | 2 | 有符号数相乘, 高位-低位(AX(16b16b)/DX-AX(32)/ECX-EAX(64))=地址2的数地址1的数 |
imul | 2 | 无符号数相乘 |
div | 2 | 有符号数相除, 商-余数(AX(16)/DX-AX(32)/ECX-EAX(64))=地址2的数/地址1的数 |
idiv | 2 | 无符号数相除 |
逻辑运算 | ||
not | 1 | 按位取反 |
and | 2 | 按位与 |
or | 2 | 按位或 |
xor | 2 | 按位异或 |
test | 2 | 地址2的数&地址1的数,数不改变大小,常用于跳转的计算 |
位移 | ||
shl | 2 | 逻辑左移 |
shr | 2 | 逻辑右移 |
sal | 2 | 算数左移 |
sar | 2 | 算术右移 |
rol | 2 | 循环左移 |
ror | 2 | 循环右移 |
rcl | 2 | 带循环左移 |
rcr | 2 | 带循环右移 |
跳转 | 均为1,跳转的目标地址 | 有条件跳转 是 根据(因test/cmp产生的)符号位的情况决定是否跳转 |
条件 | 说明 | |
jmp | 无条件 | 无条件跳转到对应地址 |
jc | CF==1 | 有 进/借 位 |
jnc | CF==0 | 无 进/借 位 |
je/jz | ZF==1 | ==0 |
jne/jnz | ZF==0 | !=0 |
js | SF==1 | 是负数 |
jns | SF==0 | 是非负数 |
jo | OF==1 | 有溢出(有符号数) |
jno | OF==0 | 无溢出(有符号数) |
下面是无符号数跳转 | ||
ja/jnbe | CF== 0 && ZF==0 | >0 |
jae/jnb | CF== 0 || ZF==0 | >=0 |
jb/jnae | CF== 1 && ZF==0 | <0 |
jbe/jna | CF== 1 || ZF==0 | <=0 |
下面是有符号数跳转 | ||
jg/jnle | SF== OF && ZF==0 | >0 |
jge/jnl | SF== OF || ZF==1 | >=0 |
jl/jnge | SF!= OF && ZF==0 | <0 |
jle/jng | SF!= OF || ZF==1 | <=0 |
栈 / 函数的调用过程
栈在程序运行时分配,从0x08048000开始,向下延申
即 栈底(EBP)在上,栈顶(ESP)在下
(EBP保存栈底,ESP保存栈底)
函数P(caller,调用者)调用函数Q(callee,被调用者)
过程调用的执行步骤如下
- P将入口函数(实参)放到Q能访问到的地方
- P将返回地址存到特定的地方,然后将控制转移到Q
- Q保存P的现场,并为自己的非静态局部变量分配空间
- 执行Q的函数体
- Q回复P的现场,并释放局部变量所占空间
- Q取出返回地址,将控制转移到P
形象地说,就是
调用者在调用函数前,保存自己的返回地址(即下一条指令的地址,保证返回时可以继续执行程序)和EAX、ECX、EDX寄存器
在调用函数后,控制权转到被调用者
被调用者将 调用者的栈底、需要用到的被调用者保存寄存器 push进栈,然后可以开辟自己的栈
运行完被调用者函数体
将 调用者的栈底、需要用到的被调用者保存寄存器 pop出栈
控制权转到调用者
pop出EAX、ECX、EDX寄存器和返回地址
继续运行调用者的函数体
调用者保存寄存器:EAX,ECD,EDX
被调用者保存寄存器:EBX,EDI,ESI
开辟的栈的大小要是16的倍数(返回地址和上一个EBP除外)
程序的链接
连接前后的文件结构
链接视图(链接前) | 执行视图(链接后) |
---|---|
ELF头 | ELF头 |
程序头表(段头表,可选) | 程序头表(段头表,必须) |
节1 | 段1 |
… | … |
节头表(必须) | 节头表(可选) |
可选的一般不需要
ELF头
包含文件结构说明的信息
节/段 头表
由若干表项组成,每一项有(节/段 名、偏移、大小、访问属性、对齐方式)
链接视图(节)
文件结构 | |
---|---|
ELF头 | |
程序头表(段头表,可选) | |
.text | 目标代码 |
.rodata | 只读数据,如printf的格式串,switch的跳转表 |
.data | 已初始化的全局变量 |
.bss | 未初始化的全局变量 |
.stmtab | 符号表 |
.rel.text | .text节的重定位信息 |
.rel.data | .data节的重定位信息 |
.debug | 调试用符号表 |
.line | C行号与.text节机器指令的映射 |
.strtab | 字符串表,包括.symtab和.debug节中的符号以及节头表中的节名 |
节头表(必须) |
执行视图(段)
段与节差别不大(知识在执行视图里,所以叫段)
与节相比,少了.rel.data和.rel.text,多了.init和.fini
- .init:在程序运行后,进入主函数之前,运行里面的指令代码
- .fini:主函数运行完后,程序结束前,运行里面的指令代码
下面的每一块根据运行时的 是否载入与**(载入后)读写权限**的不同,分为3类
- 只读(代码)段
- 可读写(数据)段
- 不需映射到存储空间的符号表和调试信息
文件结构 | |
---|---|
只读(代码)段 | |
ELF头 | |
程序头表(段头表,必须) | |
.text | 目标代码 |
.init、.fini | 在主函数运行前后运行 |
.rodata | 只读数据,如printf的格式串,switch的跳转表 |
可读写(数据)段 | |
.data | 已初始化的全局变量 |
.bss | 未初始化的全局变量 |
不需映射到存储空间的符号表和调试信息 | |
.stmtab | 符号表 |
.debug | 调试用符号表 |
.line | C行号与.text节机器指令的映射 |
.strtab | 字符串表,包括.symtab和.debug节中的符号以及节头表中的节名 |
节头表(可选) |
符号解析
符号:全局变量名和全局函数名
全局符号:非静态 函数名/全局变量名
外部符号:extern定义的符号
本地符号:静态 函数名/全局变量名
强符号:全局符号
弱符号:外部符号,本地符号
符号解析:将每个符号的引用与符号的定义建立连接
每个符号最多1个强定义(多个强定义会直接报错 ;某个符号没有强定义会指定其中的一个并警告,不会报错)
重定位
把指令里的符号与它的定义绑定起来(给一个确定的地址)
相/绝 对定位
绝对定位:R_386_32,用于函数的定位
相对定位:R_386_PC32,用于变量的定位
动态链接
使用网络上的动态链接库
程序的执行
1条命令完成需要5个步骤
- 取指令(IF)
- 指令译码(ID)
- 取操作数(OF)
- 执行(EX)
- 写回(WB)
这5个步骤分别用到了5个部件
一般指令执行采用流水线的方式,让各个部件适时都在运转(提高效率)
但流水线每一步完成之后都需要将结果 送寄存器,但总体来说,效率大大提高
单位一般为ps(10-12s)
记6个时间分别为t1-t5,t0
时钟周期:平均每条指令运行时间
流水线前:t1+t2+t3+t4+t5
流水线后:max(t1,t2,t3,t4,t5)+t0
指令吞吐率:1/时钟周期。单位一般为GIPS(每秒多少*109条指令,109instruction per seconds)
缓存(cache)
在CPU内部,用于加快内存访问速度
有时会有多级cache
- L1 cahce:1级cache,更关注速度而不要求很高的命中率。通常采用分离cache,分为d-cache(data,数据)和i-cache(instruction,指令)
- L2 cahce:2级cache,更关注命中率,不分离
- 有时也有 L3 cahce:3级cache,多个核心共用
映射
缓存分为n行(n一般=2i,每行有个行号,顺序从0开始),每行有(有效位(1b) 标记 数据(有个大小))组成
按照cache行的数据大小,对主存分块(形成了块号,以地址的部分作为块号)
这样将很抽象,举个例子(简单但不实际)
缓存16(24)行,每行的数据为4KB(212)
主存256KB(218),每字节的顺序(地址)用16b表示
/
主存被分为64(256K/4K=64=26)块,每块4KB(212),块的顺序用6b表示(块地址的前6位)
直接映射
按照cache的行数,对块分群(形成了群号),每个块在自己的群中有自己的位置
这时,cache的行 与 单个群里的块的顺序一一对应
所以只需要相应的群号作为标记即可
主存有64(26)块,缓存有16(24)行,
主存块就被分成4(22)群,群的顺序用2b表示(块地址的前2位)
标记的大小就是2b
全相联映射
块不分群,每一块都可以映射到cache的任何一行
所以只需要相应的块号作为标记即可
主存块有64(26)块,不分群
标记的大小就是6b
组相联映射
按一定数量对cache的行进行分组(形成组数)
按照cache的组数,对块分群(形成了群号),每个块在自己的组中有自己的位置
这时,cache的组 与 单个群里的块的顺序一一对应
单个群里的块可以放到对应组的任何一行
所以只需要相应的群号作为标记即可
可以把直接映射(每个cache组 1行)和全相联映射(把整个cache看成1组)看成特殊的组相联映射
假设cache 2(21)行一组,cache被分为8(16/2=8=23)组
主存块有64块,cache有8组
主存块被分为8(64/8=8=23)群,群的顺序用3b表示(块地址的前3位)
标记的大小就是3b
替换算法(策略)
cache已满但要放入新内容时 用到的算法
先进先出(FIFO,First-In-First-Out)
对每一行计数,每次+1,把最大的换掉,并将该数置0。
命中时不置0
最近最少用(LRU,Least-Recently Used)
对每一行计数,每次+1,把最大的换掉,并将该数置0
命中时置0(与先进先出的差别)
最不经常用(LFU,Least-Frequrntly Used)
对每一行的使用次数计数,把最少的换掉
随机替换
随便找一行出来换换掉
时/空 间局部性
时间性:访问地越频繁,时间性越好
空间性:访问的地址越连续,空间性越好(比如数值,对于单个变量没有空间性)
上面两个的意义是,
世界性:频繁访问,这样它会一直在cache中,而不必每次都从内存访问
空间性:从内存中取内存的时候,通常会把它后面一些的块也取进来,访问到后面的数时(比如数组),数在cache中