深入理解计算机系统

啥也没整理,鹦鹉学舌,都是书上现有的东西。

第一章

第一章啥也没有,跳了。
也许有点啥,但我白兰。

第二章

  • 的非负整数次幂,即快速转置成十六进制:

    表示成的形式,转置后的十六进制即为面跟着个十六进制的

,其中,则十六进制表示为(2的三次方跟着2个十六进制的0)

  • 十六进制跟十进制的转换和数字逻辑里一致,不赘述。写在这里是让你回想一下。

防止记不起来还是给一张图
图裂了就没办法了.png

  • 字节这块需要注意的是,32位和64位常用的数据类型中,不同的是long,在32位上是4字节,64位上是8字节。char *也有区别,但考虑的相对较少。

图裂了就没办法了.png

  • ISO C99中引入了数据大小固定的数据类型。其中就有int32_tint64_t,分别为4个字节和8八个字节。

  • 大部分的编译器和机器都认为是有符号数据类型,但实际上C标准不保证这点。所以其实需要程序员声明是有符号的。但实际上这点根本无所谓。

大端法和小端法

给一个三级标题是因为这个东西是考试重点,后面也一样。

  • 大端法
    即最高有效字节排列在前面的方式。
    是符合人类直觉的正常数字排列。
    Sun是大端法机器。
    图裂了就没办法了.png

  • 小端法
    将最低有效字节排列在前面的方式。
    linux 32和Windows和Linux 64均为小端法机器。
    图裂了就没办法了.png

  • 需要注意的一点是,实际上机器代码中按照大端和小端排列的只有立即数的部分,而表明操作的代码不按照这个来。
    (图中右侧代码从下往上读)
    3_YU_I39P0_D482U_J9I9AQ.png

对齐

(PPT上有但我怀疑不用考)

  • 按边界对齐(假定存储字的宽度为32位,按字节编址)(图上)

    • 字地址:4的倍数(低两位为0)
    • 半字地址:2的倍数(低位为0)
    • 字节地址:任意
  • 不按边界对齐 (图下)
    坏处:会增加访存次数
    A8_DRLZVA8I0YKFQ1_@DO8T.png

  • 关于对齐的样例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct S1{
    int i;
    char c;
    int j;
    }

    struct S2{
    int i;
    int j;
    char c;
    }

    在要求对齐的情况下,结构体S2优于结构体S1。
    S5_3~EA_U_Y9J_~_Q5__XJE.png

同时考虑对于struct S2 d[4]需要分配几个字节。(数组)
X_7@~3N_94__1FS55C9OU08.png

布尔运算

这个布尔运算就是与或非异或同或那一套,数电里有详细说明,这里就不赘述了。
提一下。

确定一个位级表达式的结果最好的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后转换回十六进制

注意:要区别,&和&&,~和!。(这三组中,前者都是算数运算符,后者都是逻辑运算符)
注意:|和&的逻辑运算符第二个重要的区别就是如果逻辑运算符中,如果对第一个参数求值可以得到表达式的结果,则不会对第二个参数求值。比如说a&&5/a不会导致被零除,p&&p++也不会访问空指针。

  • 掩码运算
    掩码表示的是一个位模式,即从一个字中选出的位的集合。(就是从一个长串里挑几位出来)。举一个例子,掩码0xFF(最低八位为1,其余为0)表示一个字的低8位字节。令a为一个字,则a&0xFF就表示为a的最低八位。
    这类操作非常普遍,而且有许多精彩的操作,后续讲例题的时候会提到。

移位运算

首先提一点,一个长为位的操作数,最高位表示为,最低位表示为

  • 左移
    左移操作均为逻辑左移。x << k即表示为。同时,移位操作时满足结合律的,x << k << j == x << j << k
    移位后得到[]

  • 右移

    • 逻辑右移
      逻辑右移在左端补个0,x >> k表示
      即移位后得到[ ]
    • 算数右移
      算数右移在左端补个最高有效位的值。
      即移位后得到[ ]
  • 所有机器对无符号数默认使用逻辑右移,有符号数默认算术右移。

  • Java中,x >> k表示算术右移k个位置。x >>> k表示逻辑右移k个位置。C中没有这个写法。

  • 移动k位,这里的k很大
    假设对于一个位的数据类型,如果想移动位,那么会有什么结果呢?

    在某些C标准中,会移动mod位,比如说在=32时,移动36位就是移动4位。但这在C程序中并不是一定成立的,所以最好还是让移动位数小于
    但这在Java中是一定成立,这在Java中被特别要求。

  • 关于移位的运算优先级
    移位的运算优先级低于加减法。这一点有点反直觉,因此不确定的时候最好都加上括号。

有符号数的除以2的幂

X_7@~3N_94__1FS55C9OU08.png
向右算数右移。
X_7@~3N_94__1FS55C9OU08.png
有符号数直接右移位,是除以之后向下取整。
X_7@~3N_94__1FS55C9OU08.png
加上偏置值之后右移位,是除以之后向上取整

所以表达式计算
X_7@~3N_94__1FS55C9OU08.png

IEEE浮点表示

为啥其他的不说先说这个捏。
因为这个我是真不会。

  • IEEE浮点标准的形式表示一个浮点数
  • 是符号位
  • 是尾数。其范围是,不一定对应
  • 的作用是对浮点数加权,这个权重是2的次幂(可能是负数),也不一定对应

然后有两种常见的格式,每种格式中都定死了所占据的位数。
分别为float和double。
如图:
寄
float:一位,八位,二十三位。
double:一位,十一位,五十二位。

根据的值,有以下四种情况
寄

  • 规格化的值
    此时的位模式不全为1也不全为0。
    这时要加上偏置值的形式:

    指的是此时总的符号位,所以单精度时,双精度时

    这个偏置值和有符号算数位移的偏置值算法不一样,那个是
    同时小数点在最高位的左边,也就是说整个都在小数点右边。

  • 非规格的值
    的位全为0的时候,表示的就是非规格的值。
    这种情况下:

  • 特殊情况:的位全为1

    • 如果全为0,那么表示无穷大,为1是负无穷,为0是负无穷
    • 如果不是0,那么表示的就不是一个数字。

以上就是知道的所有知识了。
下面的图是个example,用来确定思路用的。爱看不看。

寄

舍入

根本没看,完全不懂,以后补上。

浮点运算

实话实说,这一块我是根本没看懂,属于是先记下来,以后再看。

  • 浮点数的加法

    • 不具备结合性。(会导致舍入)
      比如说(其中是double,是float)
    • 满足单调性,且这一点是无符号加法和补码加法都不满足的。
      即如果有浮点数,那么对于任意值的,都有
  • 浮点数的乘法

    • 满足可交换性
    • 不具备结合性
    • 不具备分配性

强制转换

  • 执行一个运算,只要其中有一个数是无符号数,就会把其他数值全部转换为无符号数并进行运算。

sizeof函数返回的值是一个无符号数。

  • 将short转换为unsigned时,我们要先改变其大小,之后再完成从有符号到无符号的转换。
  • 从int到float,数值不会溢出,但是可能被舍入
  • 从int或者float到double,可以保留精确的数值
  • 从float或者double到int,值会向0舍入。同时,也可能会溢出。

第三章 程序的机器级表示

***的汇编。

  • 汇编代码中所有以’.’开头的都是指导汇编器和链接器工作的伪指令,也就是说都是可以忽略的废话。
    就像这种东西:
    86_Q1U_T5RV4_WJLZ_QA3_X.png

  • 关于如何用生成Inter格式的汇编代码

>linux> gcc -Og -S -masm=interl xxxx.c(文件名)

至于Intel格式和ATT的区别:
JW_DAGH0`O8EX_`6WPYS5_V.png

  • 可以在C程序中加入汇编代码来提高效率。但书上提的两种方法我都没看懂。所以不细说了。

gcc和objdump命令

连续考了两年,记一下。

  • gcc指令:

    • >linux> gcc -g aa.c -o aa
      对aa.c生成可执行文件,存储在aa。
    • >linux> gcc -c aa.c
      对aa.c生成可执行文件,并命名该文件为aa.o(可执行文件后缀为o,内容为二进制机器码)
    • >linux> gcc -s aa.c
      对aa.c生成汇编文件,命名该文件为aa.s
  • objdump生成反汇编指令文件

    • >linux> objdump -d foo > foo.s
      对foo(可执行文件)反编译,得到的文件存储在foo.s(汇编文件)
    • >linux> objdump -d foo
      对foo反编译,生成名为foo.s的汇编文件。

数据格式

这玩意儿经常用,mark一下。
__G~4J__6_QH03WH___8Z6E.png

记住bwlq,然后指针和long一致就完事了。

单精度和双精度用的是一组完全不同的指令和寄存器。所以double和int后缀相同并不会导致问题。

  • 单精度浮点4个字节,双精度8个字节,X86历史上还有一个long double的浮点形式,十个字节。但是不咋好用,精度低不好移植,用的也少。

通用目的寄存器

背!

_V1_Q_LDCWM8_GW_8KLB_OA.png

上面的63,31,之类的标识指的是对应的位长,比如说eax寄存器有32个位,rax就是64个位。借此来访问寄存器的不同位置。

低于64位的寄存器不能用来储存地址,比如说%ebx就不能用来储存地址。

  • 把数据从其他位置迁移到寄存器里时,有两条规则。
    • 1.生成1字节或者2字节数字的指令会保持寄存器剩下字节不变。
    • 2.生成4字节数字的指令会把高位4个字节置0.
    • 3.生成8个字节当然就占满了。 (2条规则当然有三点)

注意栈指针%rsp,用来指明运行时栈的结束位置。有些程序会读写这个寄存器。

操作数地址

背!

背.png

带r的就是寄存器,Imm指的是立即数,M指的是内存。
$Imm的操作数就是Imm。很直观。

注意:格式里不存在括号里同时有三个寄存器的模式。这个考了两年。

数据传送指令

背!

其实也不用背。看看就懂了。和上面说的差不多。
movabsq下面会讲。

背.png

  • 源操作数可以是一个立即数,可以在寄存器后者内存,目的操作数可以指定到一个位置,一个寄存器或者一个内存地址。

注意: x86-64加了条限制,两个操作数不可以都指向内存地址。将一个值从内存移到内存,需要两条指令,一条把值移到寄存器,一条把值从寄存器复制到目的内存。

  • 大部分时候,指令只会更新目的操作数指定的寄存器字节或者内存位置。唯一的例外是指令,会把目标寄存器的高位4字节变成0。(这个上面说过了,再说一遍)
    原因是x86-64采用的牛逼惯例:任何寄存器生成32位值得指令都会把该寄存器得高位部分变成0。

  • 关于
    常规的操作,其如果要以立即数为源操作数,那么这个立即数只能是32位的补码数字,然后把这个数符号扩展成64位。(也就是说不能直接操作64位立即数)。但可以以任意64位立即数值作为源操作数,但是只可以以寄存器作为目的

  • 将较小源值复制到较大的目的的时候:
    一般这个时候要考虑位的扩展,所以指令也有零位扩展和符号扩展两种。如下:
    是零位扩展,是符号扩展。
    反正就后缀的俩字母,前面的那个是源码格式,后面的那个是目的格式。好记。

_57DUZLF_76ZEY2V_QNQVAF.png

这里没有,也就是没有四位到八位。但实际上这个零扩展操作直接用就可以实现,这依靠于x86的牛逼惯例。
经过神的提点,教材里关于上面这句关于的语句并不严谨,实际上并不等同,只是说类似于 %eax %ebx的操作可以得到 %eax %rbx的效果。,的源操作数和目的操作数仍然受其本身的限制。

WWEHT_HZ5ZR6~24W0_WONZC.png

注意到这里头有个这个只能以%eax作为源,%rax作为符号扩展的目的,跟 %eax %rax实际上一个意思,但是写起来更短更紧凑。

  • 注意:移位指令中,使用符号拓展还是零位拓展取决于源操作数的数据结构,而不是目的操作数的数据结构。

汇编指令纠错

  • 寄存器和指令后缀不匹配/两个寄存器位数大小不一样
  • 两个参数都是内存地址
  • 用非64位的寄存器作为内存地址
  • 寄存器不存在
  • 立即数作为目的
  • 操作数的写法格式有误。如(rax,rbx,rcx)。

压入和弹出栈数据

栈顶元素时所有栈中元素地址中最低的,地址向栈底依次增大。

  • push指令是压入,pop是弹出,%rsp保存栈顶元素的地址。
    就很基础,所以放一起说了。

  • 将一个四字值压入栈中,首先需要将栈指针减8,然后将值写到新的栈顶地址。

所以pushq %rbp(把%rbq的值压入栈)等价于下面两条指令:
subq $8,%rsp
movq %rbp,(%rsp)

弹出栈同理

popq %rax等价于下面两条:
movq (%rsp),%rax
addq $8,%rsp

不过pop和push只需要一个字节,执行相同效果的两条指令要八个字节。

  • x86-64中,栈是向低地址方向增长的,所以压栈要做的是减小%rsp的值,出栈要增加%rsp的值。

算数和逻辑操作

看图

裂了就寄.png

加载有效地址(lea)

leaq这个指令和mov不同的在于,这条指令不是从指定的位置读入数据,而是将有效地址写入目标操作数。
打个比方,如果%rdx里头的值设为x,而x+8的地址内存储y,那么

leaq 8(%rdx),%rax

就是将%rax的值设为x+8,而

mov 8(%rdx),%rax

指的是把%rdx+8指向的值存入x,也就是把%rax设为y。

注意:lea的目的操作数只能是寄存器

  • 一元操作的操作数可以是寄存器/内存。二元操作的操作数可以时立即数/寄存器/内存,目的操作数可以是寄存器/内存。

移位操作

  • SAL和SHL差不多,都是左移然后在右边补零。但SHR和SAR不一样,SHR是逻辑右移,SAR是运算右移(按照最高位的值来补位)。

  • 移位操作的移位量要么是一个立即数,要么是单字节寄存器%cl中的值(只能是这一个寄存器)。
    但以%cl作为移位量时,实际的移位量会根据移位指令的数据格式从%cl中取数,由%cl中的低m位决定(二进制),这里,而更高位会被忽略,举一个例子:

    当%cl中的十六进制值为0xff时,指令salb会移7位,sabw会移15位,sall会移动31位,salq会移动63位。(反正不能超过对应数据格式的最多位数)
    同时salb只看%cl中二进制的最低3位,salw只看最低4位,依次递推。

特殊的算数操作

这块其实讲的就是怎么在64的情况下得到128位的乘积。
但好像不考。先跳了。
裂了就寄.png
这几条指令中提供的为立即数。
%rdx存储高64位,%rax存储低64位。

如果是除法,那么除完之后的商放在%rax,余数存储在%rdx。

指令clto可以把%rax的符号位复制到%rdx的所有位。这条指令不需要操作数。

条件码

  • CF:进位标志。最近的操作使最高位产生了进位或者借位。用来检查无符号位的溢出。
  • ZF:零标志。最近的操作得到的结果是0.
  • SF:符号标志。最近的操作得到的结果是负数。
  • OF:溢出标志。最近的操作导致一个补码——正溢出或者负溢出都行。

lea指令和mov指令都不改变条件码。

test指令的运算规则和ADD一样,唯一的区别是test不会更改目的操作数里的值。

  • test的典型用法:
    • 两个操作数是一样的,用来检查是正数,负数,还是0.
    • 其中一个操作数是掩码,用来指示哪些位应该被测试。

访问条件码

set指令的目的操作数是低位单字节寄存器元素之一(如al),或是一个字节大小的内存地址。以此时的标志码为根据,将这个字节设为0或者1.
set指令的作用可以根据后缀与cmp指令连用来理解。
如:

1
2
cmp %rbx %rcx 
sete %al

就是在%rbx与%rcx储存值相等时,将%al的值设为1.
裂了就寄.png
注意这里的后缀l和b指的不是操作数的大小。
注意有符号和无符号用的后缀是不同的:

  • 无符号的大于后缀是a,小于后缀是b(below)
  • 有符号的大于后缀是g(greater),小于后缀是l(less)

跳转指令

裂了就寄.png

  • jmp指令作为无条件跳转,可以是直接跳转或者是间接跳转。
    • 直接跳转就是直接跟一个地址立即数。
    • 间接跳转:
      如 jmp *%rax
      就是以%rax中的值作为跳转目标。

其余的所有条件跳转只能是直接跳转
条件跳转的后缀命名规则和set指令一致,不赘述了。

  • 跳转指令的机器编码
    先不写,这一块书上写的很迷惑。

条件传送指令

裂了就寄.png
源是寄存器/内存,目的只能是寄存器,不支持单字节传送。

其实这里还有一大堆东西,但总结一下就是教你怎么看懂汇编。
白兰不想看,不写。

栈帧

裂了就寄.png
令Q作为被调用的函数,A为调用Q指令的下一条指令的地址。PC计数器中存储下一条将要执行的指令的地址。
call Q指令会把地址A压入栈中,并把PC设置为Q的起始地址。压入的地址A是紧跟在call指令后面的那条指令的地址。对应的指令ret会从栈中弹出地址A,并把PC设置为A。
裂了就寄.png
如图。call指令也可以是间接的。

裂了就寄.png
传参时寄存器的对应顺序如图。
如果参数超过6个,那么把前六个参数放入寄存器中,剩下的参数传到栈帧,而第七个参数位于栈顶。传参是在call指令之前完成的。
通过栈传递参数时,所有的数据大小都向8的倍数看齐

根据惯例,寄存器%rbx、%rbp和%r12~%r15被划分为被调用者保存寄存器。当过程P调用过程Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用时是一样的。过程Q保存一个寄存器的值不变,要么就是根本不去改变它,要么就是把原始值压入栈中,改变寄存器的值,然后在返回前从栈中弹出旧值。压入寄存器的值会在栈帧中创建标号为“保存的寄存器”的一部分。

所有其他的寄存器,除了栈指针%rsp,都分类为调用者保存寄存器。这就意味着任何函数都能修改它们。

栈中的寄存器(栈帧的简略版)

裂了就寄.png
裂了就寄.png

  • 倒也不用全记,但顺序要记,传进来的参数按顺序都进了哪些寄存器。
    rdi,rsi,rdx,rcx,r8,r9
  • 传参只进这六个,多的就用栈帧存。
    然后还有几个特殊的:
    rax:存储返回值
    rsp:指向栈顶
    rbp:有时候用来指栈底
  • 然后被谁保存的问题:
    • 调用者(caller)保存的寄存器:
      rax,rdi,rsi,rdx,rcx,r8,r9,r10,r11
    • 被调用者(callee)保存的寄存器:
      rbx,r12,r13,r14,rbp,rsp