深入理解计算机系统 第三章 程序的机器级表示(上)

[toc]

概 述

整个第三章就是在讲汇编语言。现在的程序员完全不需要去自己写汇编语言,但是如果你可以看得懂,那么对分析代码会有很大的帮助。这一篇就总结下上半部分的知识。我写的东西是自己的总结点,不是重写书上的东西,所以没看过书请去看书。

关键词是:

  1. 历史观点和汇编语言概述
  2. 访问信息中的各种指令与指示符
  3. 算术和逻辑操作
  4. 控制,也就是条件,循环等

历史观点和汇编语言概述

程序员写出的高级语言会被编译器转化成机器代码去实现,机器代码的文本表示,就是汇编语言。最后编译器用汇编器和链接器生成二进制的代码,计算机就读懂了。具体的命令是: gcc -og -s name.c

阅读汇编代码可以理解编译器的优化能力,并且发现低效率的部分,发现各种漏洞。

编译器优化代码的时候会有不同的优化程度,这个规律和我们写的代码很相似。当追求代码的效率到一定极致的时候,这个代码会变得特别难读懂,特别难改动。如果写一个思路特别清晰,大家一看就明白的代码,那么它的效率肯定不是最高的。编译器在优化的时候有很多优化选项,-og是书上比较推荐的一种,因为这种方式能让大家看懂汇编代码,如果是-o1 或者 -o2的汇编代码,我们很可能是理解不了的

机器及代码有两种重要的抽象,分别是:

  • 指令集架构
    定义了处理器的状态,指令的格式,还有每条指令对状态的影响。x86-64让程序看起来是每条指令都是按顺序执行,做完一个再做下一个,但是其实硬件层面上是并发的执行很多命令,但是最终能保证行为上和一条一条执行的结果完全一致。
  • 虚拟内存地址
    整个内存模型被计算机认为是一个非常大的字节数组,而且操作系统让64位计算机总是认为自己有填满64位那么多的内存,显然这不现实,很多内存被标注为不可用。而且操作系统会把虚拟地址转化成实际的物理地址。

寄存器在汇编中是出现最频繁的东西,你可以理解成cpu的每个核都有一组配套寄存器。寄存器不是cache,不是内存。对于它你或许可以这样子理解,办公桌上面可以放书,旁边的书架也可以放书,如果书再多了可以放在地下室里面,对于cpu来说,寄存器它随手就取就存,所以是办公桌上面的书,cache就是书架上的书,RAM啥的就是地下室的书。

在C语言中,我们是看不到寄存器的,没办法直接操作。但是汇编语言显示的就是各类寄存器的操作:

  1. 程序计数器(PC),x86-64中等于%rip,它指向下一次要执行的指令在内存中的地址。所以cpu其实傻乎乎的,%rip指向一个指令,它就操作这个指令,然后指向下一个,它做下一个,while(true)循环永不停歇,打工人实锤了。

  2. 整数寄存器,有16个。这些寄存器可以储存地址或者整数数据。比如一个函数,传入两个参数,那么会有两个固定的寄存器去储存这两个参数的值。假设我们在这个函数中创建局部变量,那么又会有一个寄存器去储存这个局部变量的值。

  3. 条件寄存器,储存条件状态的,比如上一条指令执行了一个减法,那么这个条件寄存器里面会被保存一组状态,之后通过这个状态你就可以判断了。你可以理解为if语句是通过这个条件寄存器来判断真假的。


自学疑问和注意点--算术逻辑指令

这里面记录的是我在看书的时候遇到的问题,还有我认为应该留意的知识点

  1. 一个就6个寄存器来保存参数,如果参数过多会怎么样?
    我猜会把这些参数暂时保存在内存中,之后用的时候再取回来放在寄存器里面。暂时还没见到比6个参数更多的例子。

  2. subq $8, %rdi,请问%rdi - 8,这个8是什么意思?单位是什么?
    应该是-8字节,因为subq的单位是q(quad word,64位)

  3. movl $0x1, %eax请问这个操作有何特殊之处?
    当movl指令以寄存器作为目的时,虽然它只是复制了32位,但是这个操作会同时把高32位置零movz会置零,movs会符号位扩展

  4. 数据不允许被在内存中直接mov到内存上,其他操作例如内存到寄存器,立即数到内存均可(好像是为了硬件设计方便)。

  5. %rdi 和 %rsi 记死背硬,第一个和第二个参数。%rsp,栈指针,永远指向栈顶,我对这个东西的理解是,要执行的程序地址被一个一个压进来,然后执行完了就被弹出去,比如遇到一个函数,入栈,那么就跳转到了这个函数上面,执行完,出栈再来下一个函数。如果是递归就复杂一点,递归的话函数栈会越堆越多,所以可能溢出。

  6. leaq和mov的区别是,leaq不解引用,直接把括号里面的东西赋值过去就行了

  7. 移位量要么是立即数,要么保存在单字节寄存器里面。假设单字节寄存器里面保存了0xFF,那么不同大小的寄存器在读取移位的时候读取的位是不同的,如果只有一个字节,就读3位,两个字节读4位,以此类推,log2(总位数)所以移位量是不可能大于总位数的(会被自动余模)。

  8. (%rdi, %rsi, 4)为什么x86要搞出这么一种操作指令呢,其实就是为了计算数组或者结构体很方便,%rdi 是初始地址,%rsi 是index,4是4个字节,%rdi + %rsi * 4不就刚好是数组指针的操作


自学疑问和注意点--控制,条件

  1. 有一组条件寄存器,一直保存4种状态,你不需要知道这些东西是什么,只要知道cmp这句代码之后,条件寄存器保存了这次比较的值,一般会立马接一个setge jge等等jmp操作,就是我们写的 if/else。jmp操作要记glgreater / less 代表有符号, ababove / below 代表无符号。当然e就是相等。

  2. 跳转的指令有点奇怪,举个栗子(知道这个是为了后面第七章)

4003fa:  74  02        je  XXXXX
4003fc:  ff    d0        callq *%rax

求这个XXXXX的地址是什么(程序想跳到哪里),第一行那个74你不用看,这个应该代表je,之后02,代表+2,什么加2呢,是紧接着je的下一行代码地址 + 2,也就是 4003fc + 2 = 4003fe

  1. 说到控制就要说到两种传送方法:

第一种叫 数据条件传送(现代处理器使用), 第二种叫 控制条件分支(jmp .L1这种分支)。
正常人在读if else 的逻辑当然是,哪个true进入哪个代码块,另一个在这里就先不看了。但是计算机不一样,首先计算机使用流水线原理处理多条命令,这要求计算机必须在这个判断指令没出结果之前就选择好决定进哪个分支(啊哈哈哈哈哈是不是很懵逼)。

所以计算机只能去猜哪个正确与否,猜对了的话自然没事,但是猜错了后果非常严重,需要多花15-30个时钟来返回来重新计算。公开课上教授举例:计算机像一条大海上的重量级邮轮,很难转向。就像计算机很难把已经运行过的地方再跳回去运行一下。

这个计算机猜哪个对的方法就是,控制条件转移。

而现在我们常用的 数据条件传送就不一样了,我两个全都给算了,之后哪个对我进入哪个。

if (a < 0){
    a = b;
} else {
    a = c;
}

比如说上面这个代码,编译器可能会把b和c都先放在寄存器里面备用,在判断之前先把 b 赋值给 a,之后用cmov来判断,是否小于零,如果小于零就不做任何操作,如果false就把准备好的c赋值给a。

  1. 不是所有的条件表达式都能用条件传送编译,有一些必须用分支(L1 L2 L3等等)来编译,反例1:如果某指针可能是空指针,那就不能随便解引用,只能判断后在分支内解引用。反例2:有可能if else 的计算相当复杂,计算两次得不偿失,所以还得猜。

  2. 循环,感觉循环没啥太多可说的,就是goto跳来跳去,有两种跳跃方法。
    有个点是,编译器很多时候会直接“看出来”循环的第一次是否能运行,也就是说,汇编代码里,很可能不会去做第一个判断,而是直接就开始jmp到代码块进行运行了。

  3. switch,编译器在写switch的时候用了个小技巧。比如我的case是 100--103,那么他会先把我的case减100,让case的最小值为0,有点像归一化,然后会出现已下代码

cmpq        $3, %rsi
ja              .L8

上面的这个ja大有门道,别忘了这个是无符号比较,也就是说,如果%rsi 大于3或者小于0,那么这个ja都成立。你品你细品。


指令与指示符

说了这么多,其实直接开撸代码,大家就一下子明白了,但是之前不说这么多铺垫,一下子过来还是太突兀了。这个代码我是直接敲的书上的,毕竟要靠谱一点。

long mult2(long, long);

void multstore(long x, long y, long *dest) {
    long t = mult2(x, y);
    *dest = t;
}

下面就是汇编代码了,你看到的前面带个百分号%的东西,那就是寄存器,最常用的那些寄存器有16个,很多都有自己的独特功能,哪天我自己画个图再上传上来吧。

multstore:
    pushq %rbx
    movq  %rdx, %rbx
    call  mult2
    movq  %rax, (%rax)
    popq  %rbx
    ret

pushq 的意思是压栈,将寄存器rbx的内容压入程序栈。压栈的话就是push,这个q是quad word的意思,也就是4字 == 64字节(一个字 == 两个字节)。说到这里就要把后缀说一下。汇编里面我现在见到的后缀,从小到大,依次是 b w l q。其实这个不注意的话有些懵,因为书里面经常说 “字”,这个 “字” 代表两字节 16位,缩写是 wb代表一个字节,l代表4个字节,q就是8个字节了,也就是4个字

mov的功能就是 mov src, dest,我们可以理解成这是一个赋值,把一个寄存器的值放在另一个寄存器里面。但是要注意,一个寄存器64位,有时候可以只改变低8或者16位,而不影响前面的位(但是32位不行,movl会直接导致高32位被清0)。

call就是调用另一个函数,这个时候如果mult2这个函数要运行的话,那么我们要跳到另一个函数的。

倒数第三行这个movq,括号什么意思呢?你可以理解成 指针解引用(*) 的意思,因为所有的寄存器保存的都是0101010的机器码,假设%rax里面存着0001,那么计算机也不知道,这个0001,到底是地址0001,还是整数0001,但是一旦出现了(0001),就要去内存0001处找值。

最后程序出栈,结束。

之后还有个操作数指示符,,卧槽实在不想写了,感觉再写真的就是造轮子了,我还是写点自己的易错点吧。

你可能感兴趣的:(深入理解计算机系统 第三章 程序的机器级表示(上))