汇编call指令详解_汇编语⾔⼊门教程
学习编程其实就是学⾼级语⾔,即那些为⼈类设计的计算机语⾔。
但是,计算机不理解⾼级语⾔,必须通过编译器转成⼆进制代码,才能运⾏。学会⾼级语⾔,并不等于理解计算机实际的运⾏步骤。
计算机真正能够理解的是低级语⾔,它专门⽤来控制硬件。汇编语⾔就是低级语⾔,直接描述/控制 CPU 的运⾏。如果你想了解 CPU 到底⼲了些什么,以及代码的运⾏步骤,就⼀定要学习汇编语⾔。
汇编语⾔不容易学习,就连简明扼要的介绍都很难到。下⾯我尝试写⼀篇最好懂的汇编语⾔教程,解释 CPU 如何执⾏代码。
汇编语⾔是什么
我们知道,CPU 只负责计算,本⾝不具备智能。你输⼊⼀条指令(instruction),它就运⾏⼀次,然后停下来,等待下⼀条指令。
这些指令都是⼆进制的,称为操作码(opcode),⽐如加法指令就是00000011。编译器的作⽤,就是将⾼级语⾔写好的程序,翻译成⼀条条操作码。
对于⼈类来说,⼆进制程序太难阅读了,通过代码根本看不出来机器⼲了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞⽣了汇编语⾔。
汇编语⾔是⼆进制指令的⽂本形式,与⼆进制指令是⼀⼀对应的关系。⽐如,加法指令00000011写成汇编语⾔就是 ADD。只要还原成⼆进制,汇编语⾔就可以被 CPU 直接执⾏,所以它是最底层的低级语⾔。
来历
最早的时候,编写程序就是⼿写⼆进制指令,然后通过各种开关输⼊计算机,⽐如要做加法了,就按⼀下加法开关。后来,发明了纸带打孔机,通过在纸带上打孔,将⼆进制指令⾃动输⼊计算机。
为了解决⼆进制指令的可读性问题,⼯程师将那些指令写成了⼋进制。⼆进制转⼋进制是轻⽽易举的,但是⼋进制的可读性也不⾏。很⾃然地,最后还是⽤⽂字表达,加法指令写成 ADD。内存地址也不再直接引⽤,⽽是⽤标签表⽰。
这样的话,就多出⼀个步骤,要把这些⽂字指令翻译成⼆进制,这个步骤就称为 assembling,完成这个步骤的程序就叫做 assembler。它处理的⽂本,⾃然就叫做 aseembly code。标准化以后,称为 assembly language,缩写为 asm,中⽂译为汇编语⾔。
每⼀种 CPU 的机器指令都是不⼀样的,因此对应的汇编语⾔也不⼀样。本⽂介绍的是⽬前最常见的 x86 汇编语⾔,即 Intel 公司的 CPU 使⽤的那⼀种。
寄存器
学习汇编语⾔,⾸先必须了解两个知识点:寄存器和内存模型。
先来看寄存器。CPU 本⾝只负责运算,不负责储存数据。数据⼀般都储存在内存之中,CPU 要⽤的时候就去内存读写数据。但是,CPU 的运算速度远⾼于内存的读写速度,为了避免被拖慢,CPU 都⾃带⼀级缓存和⼆级缓存。基本上,CPU 缓存可以看作是读写速度较快的内存。
汇编指令有多少个但是,CPU 缓存还是不够快,另外数据在缓存⾥⾯的地址是不固定的,CPU 每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU 还⾃带了寄存器(register),⽤来储存最常⽤的数据。也就是说,那些最频繁读写的数据(⽐如循环变量),都会放在寄存器⾥⾯,CPU 优先读写寄存器,再由寄存器跟内存交换数据。
寄存器不依靠地址区分数据,⽽依靠名称。每⼀个寄存器都有⾃⼰的名称,我们告诉 CPU 去具体的哪⼀个寄存器拿数据,这样的速度是最快的。有⼈⽐喻寄存器是 CPU 的零级缓存。
寄存器种类
早期的 x86 CPU 只有8个寄存器,⽽且每个都有不同的⽤途。现在的寄存器已经有100多个了,都变成通⽤寄存器,不特别指定⽤途了,但是早期寄存器的名字都被保存了下来。
EAX
EBX
ECX
EDX
EDI
ESI
EBP
ESP
上⾯这8个寄存器之中,前⾯七个都是通⽤的。ESP 寄存器有特定⽤途,保存当前 Stack 的地址。
我们常常看到 32位 CPU、64位 CPU 这样的名称,其实指的就是寄存器的⼤⼩。32 位 CPU 的寄存器⼤⼩就是4个字节。
内存模型:Heap
寄存器只能存放很少量的数据,⼤多数时候,CPU 要指挥寄存器,直接跟内存交换数据。所以,除了寄存器,还必须了解内存怎么储存数据。
程序运⾏的时候,操作系统会给它分配⼀段内存,⽤来储存程序和运⾏产⽣的数据。这段内存有起始地址和结束地址,⽐如从0x1000到
0x8000,起始地址是较⼩的那个地址,结束地址是较⼤的那个地址。
程序运⾏过程中,对于动态的内存占⽤请求(⽐如新建对象,或者使⽤malloc命令),系统就会从预先分配好的那段内存之中,划出⼀部分给⽤户,具体规则是从起始地址开始划分(实际上,起始地址会有⼀段静态数据,这⾥忽略)。举例来说,⽤户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,⼀直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。
这种因为⽤户主动请求⽽划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向⾼位(地址)增长。Heap 的⼀个重要特点就是不会⾃动消失,必须⼿动释放,或者由垃圾回收机制来回收。
内存模型:stack
除了 Heap 以外,其他的内存占⽤叫做 Stack(栈)。简单说,Stack 是由于函数运⾏⽽临时占⽤的内存区域。
请看下⾯的例⼦。
int main() {
int a = 2;
int b = 3;
}
上⾯代码中,系统开始执⾏main函数时,会为它在内存⾥⾯建⽴⼀个帧(frame),所有main的内部变量(⽐如a和b)都保存在这个帧⾥⾯。main函数执⾏结束后,该帧就会被回收,释放所有的内部变量,不再占⽤空间。
如果函数内部调⽤了其他函数,会发⽣什么情况?
int main() {
int a = 2;
int b = 3;
return add_a_and_b(a, b);
}
上⾯代码中,main函数内部调⽤了add_a_and_b函数。执⾏到这⼀⾏的时候,系统也会为add_a_and_b新建⼀个帧,⽤来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。⼀般来说,调⽤栈有多少层,就有多少帧。
等到add_a_and_b运⾏结束,它的帧就会被回收,系统会回到函数main刚才中断执⾏的地⽅,继续往下执⾏。通过这种机制,就实现了函数的层层调⽤,并且每⼀层都能使⽤⾃⼰的本地变量。
所有的帧都存放在 Stack,由于帧是⼀层层叠加的,所以 Stack 叫做栈。⽣成新的帧,叫做"⼊栈",英⽂是 push;栈的回收叫做"出栈",英⽂是 pop。Stack 的特点就是,最晚⼊栈的帧最早出栈(因为最
内层的函数调⽤,最先结束运⾏),这就叫做"后进先出"的数据结构。每⼀次函数执⾏结束,就⾃动释放⼀个帧,所有函数执⾏结束,整个 Stack 就都释放了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。