写在前面:从腾讯实习回来之后,就感觉到自己的知识体系过于散乱。于是萌生了写一个自己的操作系统这样的心思,此为系列第一章,主要是讲解一些汇编知识的,内容大多从CSAPP中也可以获得。
查看程序的汇编代码
这里先放一个最简单的程序代码:
#include <stdio.h>
int main()
{
printf("hello world");
}
我们可以看到,这个是由高级语言写成的,大抵是符合我们人类阅读习惯的,但是机器确实是不认识。于是就需要将这个代码翻译成01
二进制的格式去交由计算机去运行。而在整个程序,或者说是C语言中,还有一个汇编的阶段存在着。我画了大致的一张图,方便理解:
对于这个过程感兴趣的同学可以去阅读我写过的程序员的自我修养专栏,其中有对高级语言到二进制代码转换过程的详细解释。
那么,问题来了。我们怎样去查看这些汇编代码,打开神秘的底层黑盒呢?
我们可以用以下命令去生成汇编代码:
g++ -S test1.cpp
这个会在当前文件生成test.s
文件,其本质是个文本文件,我们可以打开并查看其中内容(内容太多,这里就放主要的了)
...
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
...
注:看不懂没关系,可以先跳过,后面会逐个讲解这些内容的
上面的每一个缩进行都对应着一个机器指令。例:
//将%rbq的内容压入程序栈
pushq %rbp
如果我们使用-c 命令,gcc则会汇编这些代码:
gcc -c test1.s
这个命令会在当前路径下生成一个test.o
这么一个目标文件,同时我们可以用以下命令去查看这个.o文件中的内容:
/*
* -x:表示十六进制输出
*/
od -x test1.o
//(部分内容)
0000000 457f 464c 0102 0001 0000 0000 0000 0000
0000020 0001 003e 0001 0000 0000 0000 0000 0000
0000040 0000 0000 0000 0000 02d0 0000 0000 0000
0000060 0000 0000 0040 0000 0000 0040 000d 000c
0000100 4855 e589 8d48 003d 0000 b800 0000 0000
0000120 00e8 0000 b800 0000 0000 c35d 6568 6c6c
0000140 206f 6f77 6c72 0064 4700 4343 203a 5528
可以看到其是一串对我们而言没有意义的 “机器语言”。
当然,如果我们想要翻译机器语言的话,我们可以用一类称之为反汇编器的程序来查看它,可以用到以下命令:
/*
* -d:查看反汇编结果
*/
objdump -d test1.o
test1.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <main+0xb>
b: b8 00 00 00 00 mov $0x0,%eax
10: e8 00 00 00 00 callq 15 <main+0x15>
15: b8 00 00 00 00 mov $0x0,%eax
1a: 5d pop %rbp
1b: c3 retq
汇编中的数据格式
由于历史原因,Intel使用字来表示16
位数据类型,而不是我们现在常用的字节。所以我们现在常用的字节其实也就是半个字罢了。下图是常用的一些数据大小,其中汇编代码后缀部分(GAS)很重要!!!
就像我们刚刚举例的汇编代码一样:
movq %rsp, %rbp
在每个gcc生成的汇编代码指令都有一个字符的后缀吧,表面操作数大小,movq
就表示后面跟的操作数的大小是4字,即64位。
寄存器信息
一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器,这些寄存器用来存储整数数据和指针。 下表中详细展示了这些寄存器的信息:
64位 | 32位 | 16位 | 8位 | 作用 |
---|---|---|---|---|
%rax | %eax | %ax | %al | 存储返回值信息 |
%rbp | %ebp | %bp | %bpl | 被调用者保存 |
%rbx | %ebx | %bx | %bl | 被调用者保存 |
%r15 | %r15d | %r15w | %r15b | 被调用者保存 |
%r14 | %r14d | %r14w | %r14b | 被调用者保存 |
%r13 | %r13d | %r13w | %r13b | 被调用者保存 |
%r12 | %r12d | %r12w | %r12b | 被调用者保存 |
%r11 | %r11d | %r11w | %r11b | 调用者保存 |
%r10 | %r10d | %r10w | %r10b | 调用者保存 |
%r9 | %r9d | %r9w | %r9b | 第六个参数 |
%r8 | %r8d | %r8w | %r8b | 第五个参数 |
%rcx | %ecx | %cx | %cl | 第四个参数 |
%rdx | %edx | %dx | dcl | 第三个参数 |
%rsi | %esi | %si | %sil | 第二个参数 |
%rdi | %edi | %di | %dil | 第一个参数 |
%rsp | %esp | %sp | %spl | 栈顶指针 |
刚刚明明说总共只有16个寄存器,但是表格中总共有16*4=48
个寄存器,这是为什么呢?
这是因为对于不同大小的操作数,是没有必要用到整个寄存器的。比如说一个一字节的操作数,我就要把它占用整个%rdi
么,这就可以用%dil
这个寄存器。它其实也就是%rdi
的一部分,所以还是16个寄存器啦。
那么这里又导致了一个问题,对于生成小于8字节结果的指令,寄存器中剩下的那些字节会怎么样呢?
对此有两个规则:
- 生成一字节和两字节的指令会保持剩下的字节不变
- 生成4字节数字的指令会把高位4字节置为0
注:
我们拿ax
寄存器举个例子,它是16位寄存器。是由两个8位寄存器ah
和al
组成的。其中的低8位是al寄存器;高8位,是ah寄存器。
操作数格式
大多数的指令有着多个操作数,指示出一个操作中要使用的源数据值,以及放置结果的目的位置。源数据值可以以常熟的形式给出,或者从寄存器或者内存中取出,结果当然也能够放进去。
因此,各种不同的操作数的可能性被分为三种:
- 立即数:用来表示常数值
- 寄存器:用来表示存放在寄存器中的内容
- 内存引用:根据计算出来的地址访问某个内存位置
下图中给出了操作数格式及对应关系:
这个东西很重要,务必掌握!
下面是CSAPP中的原题,觉得自己能了可以先做一下,后面有答案:
CSAPP课后习题答案
参考文献
[1] 深入理解计算机系统 第三章 程序的机器级表示
[2] 操作系统真相还原