从零写一个编译器(十一):代码生成之Java字节码基础

项目的完整代码在 C2j-Compiler

前言

第十一篇,终于要进入代码生成部分了,但是但是在此之前,因为我们要做的是C语言到字节码的编译,所以自然要了解一些字节码,但是由于C语言比较简单,所以只需要了解一些字节码基础

JVM的基本机制

JVM有一个执行环境叫做stack frame

这个环境有两个基本数据结构

  • 执行堆栈:指令的执行,都会围绕这个堆栈来进行
  • 局部变量数组,参数和局部变量就存储在这个数组。

还有一个PC指针,它指向下一条要执行的指令。

举一个例子

1
2
3
4
5
int f(int a, int b) {
return a+b;
}

f(1,2);

JVM的执行环境是这样变化的

1
2
3
stack:
localarray:1,2
pc:把a从localarray取出放到stack
1
2
3
stack:1
localarray:2
pc:把b从localarray取出放到stack
1
2
3
stack:1,2
localarray:
pc:把a,b弹出堆栈并且相加压入堆栈

对于JVM提供的对象

1
2
3
4
5
6
7
8
9
.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则是从堆栈弹出参数,然后调用方法

1
stack: out "Hello World!"

JVM的基本指令

pusu load store

JVM的运行基本都是围绕着堆栈来进行,所以指令也都是和堆栈相关,比如进行一个乘法1 * 2:

1
2
3
bipush 1
bipush 2
imul

可以看到JVM的指令操作时带数据的类型,b代表byte,也就是只能操作-128 ~ 128之间的数,而i代表是整形操作,所以相应也会有sipush等等了

下面加入要把1 * 2打印用prinft打印在控制台上,就需要把out对象压入堆栈,此时的堆栈:

1
stack: 2 out

但是调用out的参数需要在堆栈顶部,所以这时候就需要两个指令iload、istore

istore 0把2放到局部变量队列,再把out压入堆栈,再用iload 0把2放入堆栈中

1
stack: out 2

局部变量和函数参数

局部变量

在字节码里,局部变量和函数参数都会存储在队列上

1
2
3
4
5
6
7
8
int func() {
int a;
int b;
a = 1;
b = 2;

return a + b;
}

看一下这个方法执行的时候堆栈的变化情况

1
2
3
4
5
6
7
// 执行a = 1,把1压到stack上,再把1放入到队列里
stack:
array:1

// 执行b = 1,也同理
stack:
array:1, 2

最后的return也有相应的return指令,所以完整的指令如下

1
2
3
4
5
6
7
8
sipush 1
istore 0
sipush 2
istore 1
iload 0
iload 1
iadd
ireturn

函数参数

1
int func(int a, int b, int c, int d){}

在调用这个函数的适合,函数参数就会按照顺序被压入堆栈中,然后拷贝到队列上

1
2
3
4
5
stack: a b c d
array:

stack:
array: d c b a

所以在之后的代码生成部分就需要一个来找到局部变量的位置的函数

数组

创建数组

下面这段指令的作用是创建一个大小为100的整形数组

1
2
3
sipush 100
newarray int
astore 0
  • sipush 100 把元素个数压入堆栈
  • newarray int 创建一个数组,后面是数据类型
  • astore 表示把数组对象移入队列 a表示的是一个对象引用

读取数组

下面这段指令是读取数组的第66个元素

1
2
3
aload 0
sipush 66
iaload
  • aload 0 把数组对象放到堆栈上
  • sipush 放入要读取的元素下标
  • iaload 把读取的值压入堆栈

元素赋值

1
2
3
4
aload 0
sipush 7
sipush 10
iastore
  • aload 0 把数组对象加载到堆栈
  • sipush 7 把要赋值的值压入堆栈
  • sipush 10 把元素下标压入堆栈
  • iastore 进行赋值

结构体

C语言里的结构体其实就相当于没有方法只有属性的类,所以可以把结构体编译成一个类

创建一个类

1
2
new MyClass //创建一个名字为MyClass的类
invokespecial ClassName/<init>() V //调用类的无参构造函数

例子

1
2
3
4
5
6
7
8
public class MyClass {
public int a;
public char c;
public MyClass () {
this.a = 0;
this.c = 0;
}
}

public class MyClass生成下面的代码,都是对应生成一个类的特殊指令

1
2
3
.class public MyClass
.super java/lang/Object
`

下面的则是对应属性的声明

1
2
.field public c C
.field public a I

声明完属性,就是构造函数了,首先是先把类的实例加载到堆栈,再调用它的父类构造函数,对属性的赋值:

  1. 加载类的实例到堆栈上 aload 0
  2. 压入值 sipush 0
  3. 赋值的对应指令 putfield MyClass/c C
1
2
3
4
5
6
7
8
9
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字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.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

读取类的属性

1
2
aload 3 ;假设类实例位于局部变量队列第3个位置
putfield ClassName/x I

结构体数组

下面的指令创建了10个字符串类型的数组,这时候堆栈上的对象是一个引用,指向heap上一个10个字符串类型的数组

1
2
sipush 10
anewarray java/lang/String

下面的指令则是对数组的第一个元素进行赋值

1
2
3
4
5
astore 0
aload 0
sipush 0
ldc "hello world"
aastore

所以对于我们自己定义的类也是一样的

1
2
3
sipush  10
anewarray MyClass
astore 0

下面则是对数组第一个下标生成一个MyClass对象

1
2
3
4
5
aload   0
sipush 1
new MyClass
invokespecial CTag/<init>()V
aastore

下面是对数组里的对象的属性的取值和赋值操作,只是组合了之前的指令而已

1
2
3
4
5
6
7
8
9
10
aload   0
sipush 1
aaload
sipush 1
putfield MyClass/x I

aload 0
sipush 1
aaload
getfield MyClass/x I

分支语句

JVM指令还有两个个非常重要的指令就是分支和循环指令,我们先来看分支指令

1
2
3
4
5
if (1 < 2) {
a = 1;
} else {
a = 2;
}

上面对应的JVM指令如下:

  • 先把1和2压入堆栈
  • if_cmpge指令是大于等于,即如果1大于等于2就去执行else分支
  • goto指令是跳转到相应的标签,也就是执行完if,就跳出else部分
1
2
3
4
5
6
7
8
9
10
11
12
sipush 1
sipush 2
if_cmpge branch0
sipush 1
astore 0
goto out_branch0
branch0:
sipush 2
istore 0
out_branch0:
sipush 3
istore 0

循环语句

基本的JVM指令只剩循环语句了,逻辑也不困难,基本的JVM指令相对于汇编算是非常简单了

1
2
3
for (i = 0; i < 3; i++) { 
a[i] = i;
}

上面生成的对应字节码如下(假设现在变量i在队列的第5个位置,a在队列的第2个位置):

  • 首先对i赋值
  • 再把3压入堆栈和i做比较,判断i < 3
  • 之后就是对数组的操作
  • 然后修改i的值
  • 返回loop0继续判断i < 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sipush 0
istore 5
loop0:
iload 5
sipush 3
if_icmpge branch0
aload 2 ;加载数组
iload 3 ;加载标i
iload 3 ;加载变量i
iastore ;把i的值存入到a[i]
iload 3 ;加i
sipush 1 ;把1压入堆栈
iadd ;i++
istore 3 ;把i+1后的值放入到i的队列上的位置
goto loop0 ;跳转到循环开头
branch0:

小结

这一篇主要就是了解一下Java基本的字节码,因为C语言的语法比较简单,所以只需要知道一点就足够生成代码了。所以相对于汇编来说,是非常简单的了。这样下一篇就可以正式进入代码生成部分