为了方便理解,CPU 可以简单认为是:
- 一堆的寄存器,用于暂时存放数据
- 可以执行机器指令,完成运算 / 数据读写 等操作
寄存器
CPU 有很多的寄存器,这里我们只介绍 指令寄存器 和 通用寄存器。
指令寄存器
64 位下,指令寄存器叫 rip
(32 位下叫 eip
)。
指令寄存器用于存放下一条指令的地址,CPU 的工作模式,就是从 rip
指向的内存地址取一条指令,然后执行这条指令,同时 rip
指向下一条指令,如此循环,就是 CPU 的基本工作。
也就意味着,通常模式下 CPU 是按照顺序执行指令的。但是,CPU 也有一些特殊的指令,用于直接修改 rip
的地址。比如,jmp 0xff00
指令,就是把 rip
改为 0xff00
,让 CPU 接下来执行内存中 0xff00
这个位置的指令。
通用寄存器
以 x86_64 来说,有 16 个“通用”寄存器。“通用”意味着可以放任意的数据,这 16 个寄存器并没有什么区别,但是实际上还是存在一些约定俗称的用法:
先看看这 8 个:
(这是原来 32 位架构下就有的,只是 32 位下是 e 开头的)
1 | rax: "累加器"(accumulator), 很多加法乘法指令的缺省寄存器,函数返回值一般也放在这里 |
另外还有 8 个,是 64 位架构下新增的:
1 | r8, r9, r10, r11, r12, r13, r14, r15 |
机器指令
在 CPU 的世界里,只有 0 1 这种二进制的表示,所以指令也是用 0 1 二进制表示的。
然而,二进制对人类并不友好,所以有了汇编这种助记符。
算术运算
比如这段加法:
1 | add rax,rdx |
比如这个汇编指令,表示:rax = rax + rdx
,这就完成了一个加法的运算。
通常我们用 rax
寄存器来做加法运算,但是其他寄存器一样也可以完成加法运算的,比如:
1 | add rbx,0x1 |
这个表示 rbx = rbx + 0x1
。
这里的加法运算,都是在寄存器上完成的,也就是直接修改的寄存器的值。
跳转指令
比如这段无条件跳转指令
1 | jmp 0x269e001c |
CPU 默认是按照顺序执行指令的,跳转指令则是,让 CPU 不再顺序执行后续的指令,转而执行 0x269e001c
这个内存地址中的指令。
具体来说,将指令寄存器中的值改为 0x269e001c
即可,即:rip = 0x269e001c
。
内存读写指令
比如这一对 mov
指令:
1 | mov rbp, [rcx] |
这里假设 rcx
的值,是一个内存地址,比如:0xff00
。
第一行 mov
指令,是将内存地址 0xff00
中的值,读取到 rbp
寄存器。
第二行 mov
指令,则是反过来,将 rbp
寄存器的值,写入到内存 0xff00
中。
栈操作
push
和 pop
这一对用于操作“栈”。
“栈”是内存空间中的一段地址,我们约定是以栈的形式来使用它,并且用 rsp
寄存器指向栈顶。
栈操作本质也是内存读写操作,只是以栈的方式来使用。
比如这一对:
1 | push rbp |
第一行是将 rbp
寄存器中的值压入栈,等效于:
1 | sub rsp, 8 // rsp = rsp - 8; 栈顶向下生长 8 byte |
第二行则是反过来,栈顶弹出一个值,写入到 rbp
寄存器中,等效于:
1 | mov rbp, [rsp] // 栈顶的值写入 rbp |
注意:因为栈在内存空间中是倒过来的,所以是向下生长的。