现在介绍CPU的工作原理:
CPU具有非常简单的概念,但可用于解决非常复杂的任务,It’s pretty crazy!
想象一下,从顶部到底部写下指令,就像计算机的RAM一样:
并且每一行都分配一个数字,就像内存中的某个位置有一个地址一样。在内存中它将像十六进制中地址:
hex: 0x4005db
decimal: 4195803
bin: b10000000000010111011011
1 mov rax,rsi
2 add rbx, 0x9
3 test rbx, rbx
4
5
6 mov eax,ox5
7 add eax, 0x3
8 mov ebx, 0x8
9 sub eax, ebx
..................
.....................
60 0x7234
61 0x7823
62 0x7233
63
64
65 mov eax,[60]
CPU Registers
汇编代码可以做什么?首先,需要获得8-32个固定大小的全局变量,called “registers”.
根据不同的体系结构,我们的CPU具有许多这样的寄存器。例如64位intel CPU有16、17甚至更多?他们将这些寄存器称为“全局变量”,这就是它们的本质。
Arithmetic Instructions
就像你的C或者python编程一样,你可以像变量一样使用它们。在它们当中存储一个值并对它们进行一些算术,如相加或相乘。
其中有一些寄存器是特殊寄存器,其中最重要的是"程序计数器",它告诉CPU下一步执行哪条指令。每次执行指令,都会推进程序计数器。该寄存器通常称为PC,即程序计数器,但在我们的intel x86架构上它被称为指令指针,IP = PC,或EIP或RIP-取决于16位,32位或64位模式。指令指针计时器具有下一步将执行的地址值。几乎所有的计算都是根据寄存器的简单操作来表达的。
Stack with PUSH/POP
堆栈上的推送和弹出操作。堆栈存在于内存底部的区域。有一个特殊的寄存器,总是指向堆栈的顶部,Stack Pointer = SP,ESP,RSP。堆栈的增长是向下的,它从最高地址开始,当我们推送一个值时,堆栈指针不会增加,而是会减少。反之亦然。
就像字面意思一样,我们把东西放在上面,或拿走一些东西。
内存对于汇编程序来说,就像磁盘对Ruby和Python程序一样:把内存中的东西当做变量使用,完事又把它们放回到内存中。
效率和速度
理想情况下,你会希望整个程序的运行都只使用寄存器。但是不可能因此而不将值放入内存中,有时候需要寄存器来处理更复杂的东西。
如果想要优化你的代码,为确保尽可能少的使用内存,这里不得不谈论一下“多级缓存”的概念:如果你反复的使用某个内存地址,它将会被缓存在CPU附近的一个特殊的超快速内存,这不是RAM。
Moves,jumps, Branches and Calls
jump,branch,call,jmp,jne,je,bne,be,call,这些指令的作用是直接改变程序计数器。这意味着CPU执行程序不必须再逐行进行,可以跳转到其它的地址。例如,当执行一个循环重复的任务,需要不断的跳回,这在汇编中,有不同的指令来执行此操作,它们被称为jumps,Branches and calls,它们基本上都是将指令指针更改为不同的值(在需要跳转的地方更改为将要跳转的位置)。
jumps是一个无条件的goto。
寄存器上的大多数操作,如加法和减法,都具有改变状态标志的副作用,如“最后计算的值”结果是零”。只有几个状态标志,它们通常住在一个特别的寄存器里。
Branches是基于状态标志的goto,比如,只有在最后一次算术运算为“零”时才会进行goto。在x86程序集中,这将是’je’指令。
Calls是一个无条件的goto,它将推送堆栈上的下一个地址,一个RET指令可以稍后将其弹出并继续在CALL停止的位置运行。这个调用指令非常巧妙的使用了堆栈指针。
我们可以对比考虑c中的函数调用,这在汇编中是如何做的?如果函数在很多个地方被使用,所以总会要跳回。如果只使用’jmp’指令,则必须指定确切的地址,这想想就超级麻烦,'call’指令是个不错的选择,它会将下一条指令指针推送到堆栈,当跳转执行函数完成时,将执行’ret’指令。ret会将堆栈顶部的当前值弹出到指令指针中。例如:‘pop eip’。又回到调用函数的位置继续运行。
汇编指令实际上只是数字,总所周知,计算机只能存储0和1,我们通常将其表示为数字。所以汇编代码实际上并不是’mov eax, 5’这样,实际上为’B8 05 00 00 00’。十六进制的B8指的是’mov eax’,其余数字可用于表示我们想要的数字。因此,当硬件读取此数字时,必须移动到那些数字的位置,即’eax’寄存器