从零写一个编译器(十一):代码生成之Java字节码基础
项目的完整代码在 C2j-Compiler
前言
第十一篇,终于要进入代码生成部分了,但是但是在此之前,因为我们要做的是C语言到字节码的编译,所以自然要了解一些字节码,但是由于C语言比较简单,所以只需要了解一些字节码基础
JVM的基本机制
JVM有一个执行环境叫做stack frame
这个环境有两个基本数据结构
- 执行堆栈:指令的执行,都会围绕这个堆栈来进行
- 局部变量数组,参数和局部变量就存储在这个数组。
还有一个PC指针,它指向下一条要执行的指令。
举一个例子
int f(int a, int b) { return a+b; } f(1,2);
JVM的执行环境是这样变化的
stack: localarray:1,2 pc:把a从localarray取出放到stack
stack:1 localarray:2 pc:把b从localarray取出放到stack
stack:1,2 localarray: pc:把a,b弹出堆栈并且相加压入堆栈
对于JVM提供的对象
.class public CSourceToJava .super java/lang/Object .method public static main([Ljava/lang/String;)V getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Hello World!" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method .end class
getstatic、ldc和invokevirtual都相当于JVM提供的指令
getstatic和ldc相当于压入堆栈操作。invokevirtual则是从堆栈弹出参数,然后调用方法
stack: out "Hello World!"
JVM的基本指令
pusu load store
JVM的运行基本都是围绕着堆栈来进行,所以指令也都是和堆栈相关,比如进行一个乘法1 * 2:
bipush 1 bipush 2 imul
可以看到JVM的指令操作时带数据的类型,b代表byte,也就是只能操作-128 ~ 128之间的数,而i代表是整形操作,所以相应也会有sipush等等了
下面加入要把1 * 2打印用prinft打印在控制台上,就需要把out对象压入堆栈,此时的堆栈:
stack: 2 out
但是调用out的参数需要在堆栈顶部,所以这时候就需要两个指令iload、istore
istore 0把2放到局部变量队列,再把out压入堆栈,再用iload 0把2放入堆栈中
stack: out 2
局部变量和函数参数
局部变量
在字节码里,局部变量和函数参数都会存储在队列上
int func() { int a; int b; a = 1; b = 2; return a + b; }
看一下这个方法执行的时候堆栈的变化情况
// 执行a = 1,把1压到stack上,再把1放入到队列里 stack: array:1 // 执行b = 1,也同理 stack: array:1, 2
最后的return也有相应的return指令,所以完整的指令如下
sipush 1 istore 0 sipush 2 istore 1 iload 0 iload 1 iadd ireturn
函数参数
int func(int a, int b, int c, int d){}
在调用这个函数的适合,函数参数就会按照顺序被压入堆栈中,然后拷贝到队列上
stack: a b c d array: stack: array: d c b a
所以在之后的代码生成部分就需要一个来找到局部变量的位置的函数
数组
创建数组
下面这段指令的作用是创建一个大小为100的整形数组
sipush 100 newarray int astore 0
- sipush 100 把元素个数压入堆栈
- newarray int 创建一个数组,后面是数据类型
- astore 表示把数组对象移入队列 a表示的是一个对象引用
读取数组
下面这段指令是读取数组的第66个元素
aload 0 sipush 66 iaload
- aload 0 把数组对象放到堆栈上
- sipush 放入要读取的元素下标
- iaload 把读取的值压入堆栈
元素赋值
aload 0 sipush 7 sipush 10 iastore
- aload 0 把数组对象加载到堆栈
- sipush 7 把要赋值的值压入堆栈
- sipush 10 把元素下标压入堆栈
- iastore 进行赋值
结构体
C语言里的结构体其实就相当于没有方法只有属性的类,所以可以把结构体编译成一个类
创建一个类
new MyClass //创建一个名字为MyClass的类 invokespecial ClassName/<init>() V //调用类的无参构造函数
例子
public class MyClass { public int a; public char c; public MyClass () { this.a = 0; this.c = 0; } }
public class MyClass生成下面的代码,都是对应生成一个类的特殊指令
.class public MyClass .super java/lang/Object
下面的则是对应属性的声明
.field public c C .field public a I
声明完属性,就是构造函数了,首先是先把类的实例加载到堆栈,再调用它的父类构造函数,对属性的赋值:
- 加载类的实例到堆栈上 aload 0
- 压入值 sipush 0
- 赋值的对应指令 putfield MyClass/c C
aload 0 invokespecial java/lang/Object/<init>()V aload 0 sipush 0 putfield MyClass/c C aload 0 sipush 0 putfield MyClass/a I return
完整的对应的Java字节码如下:
.class public MyClass .super java/lang/Object .field public c C .field public a I .method public <init>()V aload 0 invokespecial java/lang/Object/<init>()V aload 0 sipush 0 putfield MyClass/c C aload 0 sipush 0 putfield MyClass/a I return .end method .end class
读取类的属性
aload 3 ;假设类实例位于局部变量队列第3个位置 putfield ClassName/x I
小结
这一篇主要就是了解一下Java基本的字节码,因为C语言的语法比较简单,所以只需要知道一点就足够生成代码了。下一篇就可以正式进入代码生成部分
另外,欢迎Star这个项目!
- salesforce 零基础学习(五十三)多个文件生成一个zip文件(使用git上封装的代码)
- 考虑下列生成二进制的过程,编译器被用来生成单个单元的目标代码,链接器被用来将多个目标单元合并成一个程序二进制,链接器如何改变指令和数据到内存地址的绑定?需要什么信息从编译器传递给链接器,以协助完成链接
- 用C#代码生成一个简单的PDF文件(转)
- 用下边的代码可以生成一个联动的页子(纯手写)
- 基础算法测试——生成一个1-10之间的随机整数组合
- 一个关于防止编译器优化特定代码的问题
- LCC编译器的源程序分析(57)不同目标代码生成的接口结构
- 以Point类为基础,定义一个平面中的Circle类: 1、编写一个无参的构造函数; 2、编写一个有参的构造函数; 3、在主函数中调用无参的构造函数生成圆的实例c1,调用有参的构造函数生成圆的实例c2
- 一个简单的通过代码请求k8s生成应用的demo
- 通过一个案例教你玩转MCU代码生成工具
- 用C#代码生成一个简单的PDF文件
- 收了100元辛苦费,写了一个最简单的C#ASP.NET的3层架构例子代码,源码是通过代码生成器生成的【写程序的效率神奇的高】
- 以Point类为基础,定义一个平面中的Circle类: 1、编写一个无参的构造函数; 2、编写一个有参的构造函数; 3、在主函数中调用无参的构造函数生成圆的实例c1,调用有参的构造函数生成圆的实例c2
- Activiti基础教程--01(简介、代码生成Activiti的25张表、Activiti配置文件activiti.cfg.xml生成25张表、在Eclipse上安装Activiti插件)
- 基础算法测试——生成一个1-10之间的随机整数组合
- HBuilder的扩展插件开发暴露了一个事实:其实不能实现写一次代码实现跨平台App生成
- 帖一个代码生成工具,有兴趣的话一起来完善!
- 计算机系统要素:第十一章 编译器:代码生成
- [转载]LCC编译器的源程序分析(57)不同目标代码生成的接口结构
- 利用代码生成一个SQL数据库维护计划,并启用它