“大师,程序员不懂汇编能在这个行业混吗?”
“当然可以了,你在应用层编程,不进入底层就没问题,但是有句话说得好,‘真正的程序员应该理解程序的每个字节’,理解了汇编,会对程序和计算机系统有个透彻的理解,对学习基础知识有很大的好处!”
“那汇编很难吗?”
“不难,它比你会的任何一门语言的语法都简单!”
“哦?那我可以学习一下!你教教我吧!”
“好吧,首先,汇编就是机器语言的助记符,这你肯定知道吧?”
“这我明白,然后呢?”
“然后你要理解CPU的寄存器。”
“这....... 不就是像内存一样,一个个小格子吗?为啥要有寄存器? CPU直接操作内存进行运算不就得了?”
“寄存器至少有两个好处: 1. CPU太快,比内存快100倍,CPU等不及内存。我们把寄存器放到CPU内部,紧邻ALU(算术逻辑单元),这样信号几乎可以立即传输了。”
“2. 使用寄存器还有个额外的好处,可以让指令更短,想想看,如果一条机器指令引用了两个64位的地址,它该多长啊!”
“明白了,把地址放到寄存器中,指令会短得多,那寄存器都是叫Register 1, Register 2....... 吗?”
“那肯定不是,按照不同的用途,可以把寄存器分类:”
“晕了晕了,你还说汇编简单,光是这些莫名其名的寄存器名称就让人崩溃。”
“别担心,我概要地介绍一下,你先有个基本的印象,上图中的EAX、EBX、ECX、EDX在‘大多数情况下’可以认为是‘通用寄存器’, 你可以随便使用。”
“为什么既有EAX, 还有AX、AH、AL ,他们之间有什么关?”
“最早的Intel 8086CPU中,寄存器AX、BX、CX、DX等是16位的,16位(AX)又分为高8位字节(AH),和低位字节(AL),后来Intel 推出32位的CPU,寄存器也就扩展(Extend)到了32位,EAX就出现了!”
“那64位CPU是不是得继续扩展到64位寄存器?”
“孺子可教,x86-64 CPU的寄存器是RAX、RBX、RCX、RDX...... 哎哟,扯远了,我们接着说ESI、EDI这两个寄存器,SI是Source Index, DI 是Destination Index,你猜猜他们有什么用处?”
“Source ? Destination? 好像是复制数据时指定从某个源到某个目的地。”
“对喽,有些汇编指令是专门复制数据的,可以用上ESI和EDI。还有两个重要的寄存器EBP和ESP,是专门用来做函数调用的,我们一会儿再说。”
“好吧,大概记住了!”
“汇编确实很简单,你记住,汇编的指令主要是这三类:数据传输类,算术和逻辑运算类,控制类。”
“数据传输类就是把数据从一个位置复制到另外一个位置,比如从内存到寄存器,或者从寄存器到内存, 或者从寄存器到寄存器。”
mov ax,3210H ;将0x3210放入寄存器ax
mov ax,bx ;将bx寄存器的值放入ax
mov ax,[3640] ;将一个内存单元的值送入ax
mov [502c],bx ;将bx寄存器的值送入内存单元
“有意思,都是把右边的值复制到左边。”
“这是Intel的汇编格式,在AT&T的汇编格式中,就是把左边值复制放到右边。”
“我看到了方括号[3640]、[502c] 这表示一个内存的物理地址吗?”
“嗯,这真是个好问题,涉及到段寄存器,刚才忘了给你展示了,段寄存器在实模式和保护模式下还不一样,展开讲就太麻烦了,我们先放下,暂时认为这是某个地址吧。再来看看算术和逻辑运算。”
算术和逻辑运算类无非就是加减乘除,AND, OR, 左移,右移
例如:
add ax bx ; 把ax和bx的值相加,把结果放入ax寄存器
add ax, [37a0] ; 把ax和内存的值相加,结果放到ax寄存器
inc bx ; 对bx的内容加1
shl bx 1 ; 把bx的值左移一位
and al, 11110110b ; and操作,相当于清除位 0 和位 3 ,其他位不变
“非常容易理解,那第三类控制类指令是什么意思?”
“你想想,用高级语言写程序是不是有很多分支(if else)、循环(while)?”
“对啊,汇编中有这些指令吗?”
“没有,在CPU中实现流程控制的逻辑需要多方配合,在CPU中有很多标志位,例如著名的ZF(零标志位),如果最近的操作的结果为零,则ZF= 1,然后你就可以用另外一条语句判断ZF的值,进行跳转。”
cmp ax bx ; 比较ax 和 bx ,如果相等,就把ZF标记为1
je .L1 ; 如果ZF 为1 ,则跳转到.L1处
......代码略......
.L1 sub ax 10
“我的天啊,搞个跳转这么麻烦,还是高级语言好啊!”
“那可不,但是你也要知道,高级语言经过编译,最终都会变成汇编的形式,它是一切编程的本质!”
“嗯,现在程序可以实现顺序执行,按条件跳转,那函数调用该怎么实现?”
“终于到了关键问题了,函数的调用只使用寄存器是搞不定了,需要内存的配合,在内存中建立一个叫做栈的数据结构。”
“栈我知道,先进后出嘛!”
“在这个栈中,每个元素代表一个运行中的函数,比如有三个函数main ,add,square ,main 调用add,add调用square,那在运行时,函数栈是这样的:”
“咦,这个栈中每个元素占据的空间不一样啊?”
“每个函数可能有自己的局部变量和各种参数,那大小肯定不同, 我们把这每个元素称为“栈帧”,还记得我们刚才提到的EBP寄存器和ESP寄存器吗?现在就可以派上用场了,用他俩来指向当前栈帧的开始处和结束处。”
“看起来很有道理,但是,只有两个寄存器,函数调用可能有很多层,栈帧就有很多个,不够用啊?”
“所以,当main调用add 的时候,需要把main栈帧的开始地址(就是当前EBP的值)保存到add函数的栈帧中,这样从add返回,就能恢复main的ebp了。”
“明白了,每个栈帧的开始地址相当于一个'门牌号',写在EBP寄存器中,但是EBP只有一个,所以,需要把上个门牌号暂时保存到下一个函数栈帧中。”
“嗯,你这个比喻很到位,同理,当add 调用square,需要把add的EBP保存到square的栈帧中,以便返回时恢复!”
“如果当前函数执行完,栈帧也就不用了,在废弃掉之前,把内存中的保存的值恢复到EBP当中,并且移动ESP到上个栈帧的顶部,就OK了!”
“懂了,大师,这样仅使用两个寄存器,就能记录无穷无尽的函数调用了,真是妙啊,这办法是谁想出来的啊。”
“不知道是谁先想出来的办法,现在,你觉得汇编很难吗?”
“看起来似乎不难啊!”
“我今天给你说的只是入门罢了,还有很多细节,尤其是当你读操作系统源码的时候,涉及到大量Intel CPU的知识,实模式和保护模式的转换,页表的建立......那个时候就真的很麻烦了。”
“在哪儿能学习这些知识?”
“给你推荐一套闪客写的操作系统教程吧,里边把这些知识点都给覆盖了:”
第一部分 进入内核前的苦力活
第二部分 大战前期的初始化工作
这张图展示了整个系列的结构