第一章计算机系统漫游
第一部分 程序结构与执行
第二章 信息的表示与处理
二进制 十进制 十六进制互转
低转高:用短除法反复短除高进制取余
高转低:用高进制的幂乘以每个高进制数字
字数据大小
每台计算机都有字长,指明指针数据的标称大小(normal size),虚拟地址以字来编码,对于$\omega$位的机器虚拟地址范围为:0~$2^{\omega}-1$,程序最多访问$2^{\omega}$个字节
32位机器虚拟地址限长为$2^{32}$字节=$2^{32}/2^{30}$=$2^{2}$=4Gb,32位操作系统最多只能使用4Gb的内存
寻址和字节顺序
小端法 最低有效字节在最前面
大端法 最高有效字节在最前面
整数表示
原码,补码,反码
机器数:一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1。
真值:因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:
[+1]原 = 0000 0001
[-1]原 = 1000 0001
反码的表示方法是: 正数的反码是其本身 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
补码的表示方法是: 正数的补码就是其本身 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
对于负数, 补码表示方式也是人脑无法直观看出其数值的. 通常也需要转换成原码在计算其数值.
有符号数转无符号数公式
$$ T2U_{\omega}(x)= \begin{cases} x+2^{\omega}&{x<0} \\ x&x\ge0 \end{cases} $$
无符号数转有符号数公式
$$ U2T_{\omega}(u)= \begin{cases} u&u\le Tmax_{\omega} \\ u-2^{u}&u>Tmax_{\omega} \end{cases} $$
浮点数的表示
第三章 程序的机器级表示(汇编)
数据格式
Intel采用术语”字(word)“表示16位数据类型,因此32位数称为”双字(double word)“,64位数为”四字(quad words)“
x86_64中C语言数据类型大小
C声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
数据传送指令
movb
:传送字节
movw
:传送字
movl
:传送双字
movq
:传送四字
x86_64寄存器图
操作数指示符
立即数
:用来表示常数,在ATT格式汇编,立即数的书写方法为”$“后面跟着一个用标准C表示法的整数。不同的指令允许的立即数范围也不同,汇编器会自动选择最紧凑的方式进行编码
寄存器
:它表示某个寄存器的内容,16个寄存器的低位1字节,2字节,4字节或8字节中的一个作为操作数,这些字节数分别对应于8位,16位,32位或64位,我们用符号$r_a$表示任意寄存器,用$R[r_a]$来表示他的值,这是将寄存器集合看成一个数组R,用寄存器标识符做索引
内存引用
:它会根据计算出的地址访问某个内存位置,因为将内存看成一个很大的字节数组,用符号$M_b[Addr]$表示对存储在内存中从地址$Addr$开始的b个字节的引用。
操作数寻址表
$M$:主存寻址操作 $R$:寄存器寻址操作 $r$:为寄存器 $r_b$:为偏移量 $Imm$:为基地址
类型 | 格式 | 操作数值 | 名称 |
---|---|---|---|
立即数 | ${$Imm}$ | $Imm$ | 立即数寻址 |
寄存器 | $r_a$ | $R[r_a]$ | 寄存器寻址 |
存储器 | $Imm$ | $M[Imm]$ | 绝对寻址 |
存储器 | $(r_a)$ | $M[R(r_a)]$ | 间接寻址 |
存储器 | $Imm(r_b)$ | $M[Imm+R[r_b]]$ | (基地址+偏移量)寻址 |
存储器 | $(r_b,r_i)$ | $M[R[r_b]+R[r_i]]$ | 变址寻址 |
存储器 | $Imm(r_b,r_i)$ | $M[Imm+R[r_b]+R[r_i]]$ | 变址寻址 |
存储器 | $(,r_i,s)$ | $M[R[r_i]\cdot s]$ | 比例变址寻址 |
存储器 | $Imm(,r_i,s)$ | $M[Imm+R[r_i]\cdot s]$ | 比例变址寻址 |
存储器 | $(r_b,r_i,s)$ | $M[R[r_b]+R[r_i]\cdot s]$ | 比例变址寻址 |
存储器 | $Imm(r_b,r_i,s)$ | $M[Imm+R[r_b]+R[r_i]\cdot s]$ | 比例变址寻址 |
压入和弹出栈数据
在x86-64,程序栈放在内存的某个区域中,栈向下增长,这样一来栈顶元素的地址是所有栈中元素最低的。
指令 | 效果 | 描述 |
---|---|---|
pushq S | $R[\%rsp]\leftarrow {R[\%rsp]-8};\\ M[R[\%rsp]]\leftarrow S$ | 四字压栈 |
popq D | $D\leftarrow M[R[\%rsp]];\\ R[\%rsp]\leftarrow R[\%rsp]-8$ | 四字出栈 |
指令pushq %rdp等效于以下指令
|
|
指令popq %rax等效以下指令
|
|
加载有效地址
加载有效地址的指令形式从内存读数据到寄存器,但实际上根本没有引用内存,他的第一个操作数看上去是一个内存引用,但该只看并不是从指定位置读入数据,而是将计算出有效地址写入到目的操作数,mov传送的是地址内容,lea传送的是地址
使用lea+mov实现C语言指针
|
|
|
|
5-6行,lea操作将-12(%rbp)
(即a的内存地址)放入rax寄存器,然后将rax寄存器内存储的地址放到内存中a的地址偏移4字节处
一元和二元操作
指令 | 描述 |
---|---|
add | 加法操作 |
sub | 减法操作(目的操作数-原操作数) |
imul | 有符号乘法操作 |
mul | 无符号乘法操作 |
idiv | 有符号除法 |
div | 无符号除法 |
inc | 加1操作 |
dec | 减1操作 |
clto | 转换8字 |
控制
条件码
过程
运行时栈
C语言过程调用机制的一个关键特征(大多数其他语言也是如此)在于使用了栈数据结构提供的后进先出的内存管理机制. x86_64的栈地址是向低地址方向增长,而栈指针%rsp指向栈顶元素。通过push指令和pop指令将数据存入栈中或是从栈中取出。将栈指针减小一个适当的量可以为没有指定初始值的数据在栈上分配空间,类似通过增加栈指针来释放空间
当x86_64过程需要的存储空间超过寄存器能存放的大小时,就会在栈上分配空间,这个部分称为过程的栈帧