先铺垫几个计算机的基础知识:
编译器后端的结果就是生成目标代码,如果目标是计算机那么目标代码就是汇编代码;如果目标是虚拟机,那么目标代码就是对应虚拟机的代码。
ir就是中间代码形式,java字节码,llvm,ast都是ir
ast可以叫做前端ir,java字节码叫做虚拟机的ir。
ir的目的在于做成中间代码形式而不是最终汇编代码。对于后端来说意味着新出一个语言不需要关心编译器后端去适配不同机器平台的这部分工作量了。
也就不需要根据不同平台转换成汇编,只要转换成ir就行,有专门做ir转换的程序 只需要一次转换ir就好,ir转换其他不同目标平台的汇编机器码啥的是ir做的。
编译器后端将前端生成的ast转换为ir,然后转换为不同机器平台的汇编代码。
编译器的后端是要把高级语言翻译成计算机理解的语言。
前端关注的是正确反映代码的静态结构(AST),后端关注让代码更好更快运作的动态结构(目标代码)
后端更加强调运行性能(比如公共表达式删除,寄存器优化,流水线,计算机并行能力,高速缓存等技术)
没有操作系统的时候,使用的内存地址就是物理内存要管理好自己使用的内存;但是操作系统出现后,操作系统会给程序分配一段虚拟的内存空间,64位的机器所能表示的所有内存地址叫做寻址空间(64位的寻址空间是2的六十四次方好几个t),当程序使用内存的时候操作系统会将虚拟地址映射到真实的物理地址上(可能一块物理地址被多个进程共享 共享资源真实物理内存保存一份即可),对于物理内存上不常用的内存数据操作系统会写到磁盘上腾出更多的物理空间当需要这块数据时再从磁盘写回
不同的编译器对于内存管理机制模式也有不同,不过大多数语言会采用一些通用的内存管理模式:
一般来说是只读的,不过现代语言越来越动态化,这块内存在运行时也可以将中间代码转换成机器码存放
这些数据的地址在编译期就可以确定,生存期从程序开始到程序结束
先是存储返回值(占位符),然后是返回地址(执行完该函数返回原函数继续执行),最后才是函数参数。
为什么这样做:是因为这样先清除的就是函数参数而不是返回值,如果先把参数压栈再把返回值压栈,那么清除空间的时候先清除的就是返回值而返回值一会还要用,所以不能这样做。而是把参数返回值调换位置。
方法栈调用完后按理来说操作系统应该回收这部分物理内存让其他程序使用,但是比如返回值这种数据 虽然栈帧结束了但是 程序依然要访问
所以调用约定规定程序的栈顶之外仍然会有一小部分内存(比如128kb)是程序可以继续访问的,这部分内存可以存放返回值,并且这部分内存操作系统不会回收
背景信息:约定表示 rbp寄存存放的是 栈底 的值,rsp存放的是 栈顶的值。栈是高地址向低地址增长的,栈顶是低地址,栈底是高地址。
一句话总结:函数入栈出栈操作的就是rbp和rsp这两个寄存器的值,入栈rsp增长rbp设置为rsp,保存原rbp的值;出栈rsp设置为rbp,rbp恢复
保存旧函数的rbp值(后续恢复原函数rbp)
2.rsp增长(这部分空间保存内存中新加入的栈帧)
函数入站后rbp,rsp寄存器的值变化:rsp的值增长向低地址空间扩展,rbp的值是上一个rsp的值
思路可能没有那么直观,其实很简单,就是保存调用处的地址因为你调用完方法之后还有恢复,然后申请新的内存空间保存新的栈帧,所以rsp会增长(这里增长是-多少个字节,高地址向低地址延伸)
2.之前保存的rbp恢复
3.pop把之前保存的调用处地址拿到然后跳转到对应地址执行。
函数出站rbp,rsp的值变化:
同样这个地方就更简单了,就是一个字 恢复,把原函数的rbp设置为之前保存的值,rsp设置为现在rbp的值。
默认情况下 参数传递是通过寄存器来传递,x86-64架构规定 六个以内的参数传递都是通过寄存器,超过六个用栈来传递(超过的参数在栈中倒序存放,先入站参数8,再入站7这样)
注意点:不是在新栈的内存空间内而是在新栈和调用栈中间的内存部分。
上面的实现方式已经介绍过了,因此对于操作六个以外的数据需要利用栈来操作,而这些数据是有顺序的所以需要偏移量拿到这些数据。
操作参数的汇编码:操作六个以内的参数是通过%edi使用寄存器的语法,操作六个以外的参数是通过间接地址访问的,新栈的rbp地址加上数据类型字节x参数个数(不严谨,只代表其余参数是存储在rbp栈底的上面内存空间中)
******
后端编译器转换ast为汇编:识别ast语义信息(此处上下文信息越多,后面生成的汇编码效率越高,不需要额外推断)进行标签类型匹配,然后根据ast中对应语义信息携带的上下文生成汇编码。
编译器后端将高级语言转换成汇编代码,汇编器将汇编代码转换成二进制目标文件,链接器将汇编代码和二进制目标文件链接绑定到汇编代码中
将公用的逻辑和类库抽取成单独的二进制目标代码,在其他的上层语言代码中直接使用(只是定义用extern关键字代表使用的是外部函数,当前模块不知道是否有这个函数,得等到所有模块和编译时携带的目标文件都编译完后才能知道是否有这个方法)
因此汇编器在编译汇编码到二进制文件时,得等到所有模块都编译完再*通过链接器链接模块中使用的具体的外部函数地址。 *当前文件不知道是否有这个函数,得把参数上带的所有二进制文件全部编译玩才能知道是否有,才会给使用的外部函数分配地址,才会进行链接,使用方才能正常使用
最终用的都是地址,地址在前期是不可知的因为还没有编译不知道存放在哪个地址,只有都编译玩后才能知道再替换
java的链接过程也是一样,符号只是代表使用某个标签,等对应标签的地址分配好时要替换到符号处,符号使用的时候才能跳转到正确的地址执行
关于数据表示的几种方式
定义:带有括号的表示,比如(%rbp)代表的就是rbp寄存器中存放的值指向的地址(也就是说这个寄存器中存放的是内存地址)。间接访问就是基于这个寄存器中存放的值进行偏移。
比如4(%rbp)这个意思就是rbp中存放的地址加4个字节的地址的值。同样也是基于基址进行增长减小。
常见的表示公式:(基址,索引值,字节数)
最终的地址公式是这么计算的:基址+索引值*偏移量+字节数。
比如(%rbp,%rsp, 4)这个地址没有偏移量,因此是基于rbp的值(存放的是内存地址)增长4个rsp(这个存放的是索引值,123)的值。rbp+rap*4。很明显这是一个数组中的值
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。