深入理解计算机系统学习笔记

#导论#

程序编译的过程

  1. 预处理阶段,将include包含的内容直接插入到文本中间。
  2. 编译阶段:将程序转为汇编代码编写的程序、
  3. 汇编阶段:将汇编代码转为机器指令,输出目标文件、
  4. 链接阶段:负责将所引用库中已经生成好的目标文件合并到汇编阶段所产生的目标文件中,最后生成可执行目标文件。

设备的存储级别
寄存器-》L1,L2,L3缓存-》内存 -》磁盘-》分布式文件系统
从下到上提供缓存作用

虚拟内存的地址空间

程序代码地址 -》程序运行时堆 -》 共享库地址-》栈-》内核地址空间

对于所有代码来说,代码都是从一个固定地址开始。

并发:
线程级别并发,利用现代CPU的超线程技术,一个CPU核心可以同时执行两个线程。

指令级别并发:
现在处理器上,处理器可以同时执行多条指令

单指令,多数据并行:允许一条指令,产生多个可以并行的操作。

抽象:
文件是对I/O的抽象,虚拟存储是对I/O和内存的抽象,进程是对I/O,内存,处理器的抽象。

#信息的表示处理#

在C/C++中,指针实际上指向了一个存储块的虚拟地址的第一个字节,使用指针的时候,指针实际上包含了指向地址的长度。例如在int*指针中,指针所指向地址的长度是4,我们可以将指针的类型的改为char* 这样,我们可以访问这个int中的每一个字节。

字长是单位是字节,32位的电脑是指2^32字节。

##字节序##
大端法:高位有效字节在前面。
小端法:地位有效字节在前面。

计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

##位操作##
基本的位操作:异,或,与,左移,右移
逻辑右移:左边的高位,补0.
算数右移:左边的高位补上k个(右移k位)最高的有效位。(目前基本上都是采用的算术右移)

【Java当中】当移动的位数k很大的时候,通常会通过 k % sizeof(type) 之后 再来进行真正的移动,比如int 类型的左移36,那么实际上真正移动的只有4位 (36 % 32)。

【在部分c/c++中】当对一个数进行左移的时候,如果超过了这个数的字长,那么这个数将会变成零, 例如 32位的int位移大于等32的时候会变成零。

在C++中,对任何一个非零的数执行 ! 运算都是1。

##整数的表示##

  1. C/C++中,数据支持有符号和无符号的,但是在Java中只支持有符号的数
  2. 当有符号数和无符号数之间发生转换的时候,从有符号数转为无符号数,如果是正数那么实际上的数值还是原来的数值,如果是负数,转为无符号的数之后实际上只是对位的解释方式不同了,有符号的数之前是最高有效位是负权值也就是(-2^(w-1)),转为正数之后实际上由负权值便为了正权值,所以两者时间的值实际上是相差了(2 * 2^(w-1) = 2 ^ w),所以在强转之后,可以通过很简单的公式来计算强制转换的结果。

    static void testSigned() {
        unsigned char u =  0;
        u = -1;
        cout<<hex<<(u - 0)<<endl;
    }
    

    上面的结果会输出 ff 也就是 2^8 + (u),

  3. 在C/C++语言中,当无符号数和有符号数一起运算的时候,会先将有符号数转为无符号数,然后再开始运算,在计算的过程中应该注意此类陷阱。

    static void testSignedOperation() {
        cout<<(-1<0u)<<endl; // 输出 false;以为 0u是无符号数
    }
    

扩展数字的位表示(向上数据类型的转换)
扩展一个数位的表示就是通常我们在代码里面写的对基本数据类型的向上转换,对于一个无符号的数,只需要在数据位钱补零就行,但是对于一个有符号的数字来说,则需要补充最高的有效位。

截断数字的表示(向下的数据类型转换)
截断数字的表示通常会丢掉k个高位。对于无符号的数字可以直接按照剩下的bit串来解释数字,将其转为对应的数字表示即可,但是对于有符号的数字,我们还需要注意其表示范围,例如对于int 53191,转为short之后,因为是有符号的类型,所以剩下的位串最终会被解释为-12345

运算溢出
当两个数进行运算后,如果出现了溢出的情况,这个时候需要根据两个数的符号类型(有符号还是无符号),对截断后的位串重新进行解释。例如在4位情况下,-8 + (-7) = -15,位串表示为 10001,这个时候需要截取位串,去掉头部的1, 剩下 0001,所以结果为1.

整数的运算
乘法:整数的乘法通常会比加法耗费更多的时间的时间周期,所以通常可以把乘法拆分成位移运算和加法运算的结合。

例如x 3, 我们可以将3 拆分成 2^1 + 2^0,
x
3 = x (2^1 + 2^0) = x 2 + x * 1 = x << 1 + x << 0

除法:对于程序中的除法,所有的结果都是向零取整(意味着对于一个结果大于零的数字,这个值是向下取整,但是对于一个小于零的结果,这个值是向上取整)

-5 / 2 = -2;(结果小于零,向上取整)
5 / 2 = 2; (结果小于零,向下取整)

浮点数

  1. 浮点数的计算不满足结合律,分配律,所以在计算的时候需要额外考虑一点。
  2. 浮点数的表示为 V = s E M, (符号位 阶码 尾数)

#程序的机器级表示#

Linux使用平坦寻址的方式,使程序员将存储空间看做一个巨大数组。

机器级代码

  1. 对于机器级编程来说,两种抽象至关重要,第一种是机器级程序的格式和行为,定义为指令集体系结构(ISA), 它定义了处理器状态,指令的格式,以及每条指令对状态的影响。 第二种抽象是机器级程序使用的地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。

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

  3. 程序链接之后,所有变量在内存中的地址就确定了。

数据格式

  1. 字 16位, 字节 8位,双字 32 位。

  2. 三类操作符: 第一种是立即数,用$ 加上 一个C表示的常数,例如movl $66, %eax, 就是将常数66放入寄存器eax中。第二种操作的对象是寄存器, movl %edx, %eax,例如就是将寄存器edx中的值拷贝到eax,第三种操作的对象是存储器的引用(这里的引用可以改成索引), movl (%edx), %eax, 假设内存数组为M, 这里movl 操作的对象是M[%edx] 这个地址。

  3. 在内存中程序所使用的地址是向下增长的。

  4. 数据传送指令

    数据传输指令S,D 不能同时指向存储寄存器。

  5. %esp寄存器指向的地址总是栈顶,而在栈顶之外的任何数据都认为是无效的。

  6. 算术和逻辑操作

leal 指令的第一个操作数看上去是一个存储器引用,但是该指令并不从指定的位置读取数据,而是将有效地址写入目的操作数。

  1. 特殊的算术操作

IA32 中提供了两种不同的乘法操作imull 和 mull,这两种指令都要求有一个参数存放在%eax寄存器中,而另一个操作数作为源操作数给出,然后乘积存放在%edx (高32位) 和 %eax(低32位),虽然imull有两种用法,但是汇编器可以通过计算操作数的数目来辨别应该使用那种命令。

  1. 条件码寄存器:CPU维护一组条件码寄存器,他们描述了最近算术和逻辑运算的属性,可以检测这些寄存器来执行条件分支指令。
  • CF: 进位标志,表示最近的操作使最高位产生了进位
  • ZF: 零标志,表示最近操作得出的结果为零。
  • SF: 符号标志,表示最近得到的操作结果为负数
  • OF: 溢出标志,表示最近的操作导致一个补码溢出。