x86 汇编语言
看过 CS61C 的朋友应该已经很熟悉汇编语言了,但是 x86 汇编还有编程模型与 RISC-V 还是有明显区别的
x86寄存器
unsaved GP 寄存器:
eax (ah,al)
ecx (ch,cl)
edx (dh,dl)
esi (extended source indicator)
edi (extended destination indicator)
saved GP 寄存器
ebx (bh,bl)
ebp (extended base pointer)
特殊寄存器
esp (extended stack pointer)
段寄存器 (cs,ds,ss,es)
其中,esp 寄存器存放堆栈指针,对应 risc-v 中的 sp 寄存器
x86 没有返回值寄存器,返回地址存在堆栈中
段寄存器用来存放段地址,在 8086 寻址时常用,用来扩大寻址范围,但是在 ia32 之后内存基本上为平铺,不用考虑段地址
调用规范
当汇编语言需要调用子程序时,调用规范就显得很重要了,特别当汇编语言和其他高级语言进行混编的时候,需要特别指明汇编程序的调用规范
STD CALL
当程序员要调用子函数时,会将子程序的参数,从右至左依次入栈,然后将返回地址入栈,返回地址为调用地址 + 一个 word。std 调用规范要求子程序编写者维护调用时的堆栈,如果子程序有三个参数,调用者将三个参数依次入栈,那么要求编写者在完成子函数后将堆栈指针向上移动三个 word,然后返回。
C
C 调用规范和 STD 规范不同的是,C 子程序的调用者必须自己维护堆栈,比如子程序有三个参数,子程序调用者将三个参数依次入栈,然后将返回值入栈。子函数不会改变调用者的堆栈,当结束运行子函数后仅仅会返回到调用地址 + 一个 word,C 子程序的调用者必须自己将堆栈指针 + 三个 word. C调用规范的优点在于,调用者不规定参数的数量,即参数可以是任意多个,给 printf 这样的函数提供了便利。C 调用规范的缺点也很明显,用户很容易把堆栈搞砸。
栈帧(stack frame)
当我们运行程序时,为了可以让不同的子程序互相使用寄存器资源,我们将调用者的资源入栈。为了追踪程序运行的方向,当我们调用子程序时,会将返回地址入栈。每一个子程序所涉及的堆栈叫做一个栈帧,栈帧记录了我们函数调用的过程,比如我们依次在 main 中调用了 func1,在 func1 中调用了 func2,在 fun2 中调用了 func3 我们堆栈就会像这样。
…………
(paramters for main)
(main return address)
(main local variables) ——> main stack frame
(paramaters for func1) ——> main stack frame
(func1 return address) ——> main stack frame
(func1 local vaiables) ——> func1 stack frame
(paramaters for func2) ——> func1 stack frame
(func2 return address) ——> func1 stack frame
(func2 local variables) ——> func2 stack frame
…………
当我们返回时,这个函数的 stack frame 就应该被丢掉,在 STD 调用规范中,子程序编写者会自动将堆栈指针恢复到调用处,但不会影响调用者的堆栈 比如调用的参数 。对于 C 调用规范来说,子函数会将自己的局部变量移除堆栈然后返回,至于调用参数移出堆栈就是调用者自己的责任了。
C 语言
如果你先了解 x86 汇编语言,然后再了解 C 语言,那么 C 语言基本上就是 x86 汇编语言的升级版,因为 x86 汇编语言的多样性,这两种语言几乎是一模一样。唯一的区别就是 C 语言会帮助你管理寄存器和堆栈,虽然是两个不同等级的语言,但是就语法和语义上来说,他们几乎是一对一的。
如果你对 C 语言是如何翻译成 x86 汇编语言感兴趣的话,godbolt compiler explorer 也许可以帮到你。
http://godbolt.org