java开发C语言编译器:把printf编译成java字节码的算法详解

更详细的讲解和代码调试演示过程,请点击链接

上一节,我们探讨了jvm函数调用时,参数是如何传递的。上节对参数传递的方式有个错误,这里先更正一下。在上节,我是这么说明jvm函数调用时的参数传递方式的:

static double lotsOfArguments(int a, long b, float c, String[][] d) {
    ....
}

当上面函数运行时,在执行函数lotsOfArguments前,jvm会把输入参数全部放到堆栈上,当函数被执行时,参数会从堆栈拷贝到局部变量队列,因此当lotsOfArguments执行前,堆栈上参数如下:
stack:
d
c
b
a
函数执行时堆栈上的参数会依次拷贝到局部变量队列,情况如下:
local_list: d c b a 。

上面说法错误在于,参数从堆栈拷贝到局部队列的次序说反了,参数拷贝到队列后的次序应该是:
local_list: a b c d.

由此,当我们的编译器把C含有传递参数的函数调用的代码编译成java字节码时,需要注意处理上面所说的参数传递的次序,上一节,我们使用了一个名为:getLocalVariableIndex(Symbol symbol)的函数来查找函数输入参数对应在局部变量队列上的位置,这里我们需要对其实现根据上面的修改来更正一下,好在需要改正的代码不多,内容如下:

public int getLocalVariableIndex(Symbol symbol) {
        TypeSystem typeSys = TypeSystem.getTypeSystem();
        String funcName = nameStack.peek();
        Symbol funcSym = typeSys.getSymbolByText(funcName, 0, "main");
        ArrayList<Symbol> localVariables = new ArrayList<Symbol>();
        Symbol s = funcSym.getArgList();
        while (s != null) {
            localVariables.add(s);
            s = s.getNextSymbol();
        }
        Collections.reverse(localVariables); //相比上节代码,我们只需添加这行就能改正上面所说的错误
        
        ArrayList<Symbol> list = typeSys.getSymbolsByScope(symbol.getScope());
        for (int i = 0; i < list.size(); i++) {
            if (localVariables.contains(list.get(i)) == false) {
                localVariables.add(list.get(i));
            }
        }
        
        for (int i = 0; i < localVariables.size(); i++) {
            if (localVariables.get(i) == symbol) {
                return i;
            }
        }
        
        return -1;
    }

说到有输入参数的函数调用,在我们的C语言代码里,用到最多的是printf,因此有必要就该函数如何编译成java字节码再进行详细的解析。假设C代码中有这样一条语句:

printf("the value of a, b , c is: %d, %d, %d", a, b, c);

我们知道,在Java中对应printf函数的是System.out.print,我要在jvm中实现上面printf函数的功能,我们需要先把对象System.out压入到堆栈,然后再把要打印到控制台上的变量内容压入到堆栈上。我们先看一个简单点的版本:

printf("%d", x);

上面的代码本意是要把整形变量x的值输出到控制台,但我们实现的编译器在解析上面代码时,会先解析变量x, 当解析到一个变量x时,假设变量x在局部队列中的次序是0,那么我们当前实现的编译器一旦解析到变量x时,会先输出一条java字节码指令:
ILOAD 0

解析完变量x后,编译器才会明白printf不是一个变量,而是一个函数名称,这时我们的编译器才会知道,要把printf与java对象System.out对应起来,于是就会把System.out对象压入到堆栈上,因此就会输出java字节码指令:
getstatic java/lang/System/out Ljava/io/PrintStream;

这样的话就会把System.out对象压入到堆栈上,此时堆栈的情况如下:
stack:
System.out
x

这样一来就有问题了,要想正确打印x变量的值,堆栈上的情况应该是:
stack:
x
System.out

也就是说两者在堆栈上的位置发生了颠倒。为了处理这个问题,需要修改一下编译器的编译流程,当编译器先解读了变量x,导致x的值先压入堆栈,当编译器接着解读到printf时,我们必须先把变量x从堆栈上,因为变量x是从局部变量队列加载到堆栈上的,也就是说上面执行完语句ILOAD 0 后,变量x同时存在在堆栈和队列上:
stack:
x
local_list: x

此时,我们需要把x从堆栈上挪开,挪开后放到哪呢?我们把它挪开后,放到局部变量队列的末尾,因此为了把x挪到局部变量队列的末尾,我们接着执行指令:ISTROE 1, 于是堆栈和队列的情况在执行指令后如下:
stack:
null
local_list: x , x
也就是说,此时变量x的值在队列上有两份,你会疑问直接把x的值从堆栈顶部弹掉,压入System.out对象后,再次把x从队列里面再压入堆栈不就可以了吗,亦或者我们把x从堆栈顶部挪开时,直接挪回x原来在队列中的位置,也就是执行ISTORE 0 不就可以了吗。这么做是不可以的,因为如果是这种情况:

printf("%d", x+1);

此时我们要输出的是x+1的值,输出后变量x的值是不变的,编译器解读上面代码时,会先解读到x+1,于是它会把这个表达式的值压入堆栈,形成如下情景:
stack:
x+1

local_list: x

因此如果直接把堆栈顶部元素弹开,那么x+1的值就找不来了,如果直接把堆栈顶部的内容存回到x变量在队列中的位置,那就好变成:
stack:
null
local_list: x+1
这样的话,变量x的内容就改变了,因此也不行,所以要把堆栈顶部元素的值存到局部变量队列的末尾,也就是执行指令ISTORE 1:
stack:
null
local_list: x, x+1

接着把System.out压入堆栈,也就是执行指令:
getstatic java/lang/System/out Ljava/io/PrintStream;
这时候情况如下:
stack:
System.out
local_list: x, x+1
这时候,再把队列末尾的元素压到堆栈顶部,也就是执行语句ILOAD 1,于是情况变成:
stack:
x+1
System.out
local_list: x, x+1

此时再调用System.out对象的print(I)V方法,也就是打印一个整形的方法,因此对应的指令就是:
invokevirtual java/io/PrintStream/print(I)V

所以综合起来说,当编译器编译语句:

printf("%d", x);

成为java字节码时,编译后的代码为:

iload 0 
istore 1
getstatic   java/lang/System/out Ljava/io/PrintStream; 
iload 1
invokevirtual   java/io/PrintStream/print(I)V

我们再看复杂一点的情况:

int f(int a, int b, int c, int x) {
    printf("%d, %d, %d, $d", a, b ,c , x);
    int d = x;
    return d;
}

该函数有4个输入参数,因此函数在jvm执行时,四个输入参数会放置在局部变量队列上:
local_list: a, b , c , x, d
变量a处于局部变量队列的低0处,变量b处于队列位置为1处,变量c处于队列位置为2处,变量x处于队列位置为3处。由于函数还有一个局部变量d,因此变量d在队列的末尾,也就是处于队列位置为4处。

在编译器解读语句printf("%d, %d, %d, %d", a, b, c, x);时,根据我们前面的讨论,编译器会先解读函数的输入参数,也就是先解读变量a,b,c,d,因为每解读到一个变量是,我们的编译器会自动执行iload语句,把变量加载到堆栈上,因此就产生了如下指令:

iload 0  ;解析变量a
iload 1  ;解析变量b
iload 2  ;解析变量c
iload 3  ;解析变量x

上面指令执行后,jvm堆栈和队列情况如下:
stack :
x
c
b
a
local_list: a, b , c, x , d

根据前面讨论,我们需要先把变量从堆栈上挪到队列的末尾,因此编译器要执行指令:

iload 5  ;把x放到队列位置为5处
iload 6  ;把c放到队列位置为6处
iload 7  ;把b放到队列位置为7处
iload 8  ;把a放到队列位置为8处

于是堆栈和队列的情景如下:
stack:
null
local_list: a, b , c, x, d, x, c, b , a

这时,编译器再把System.out压入堆栈,也就是执行指令:

getstatic   java/lang/System/out Ljava/io/PrintStream;

接着把处于位置8处的变量a的值再重新放回堆栈,也就是执行指令ILOAD 8,此时运行环境为:
stack:
a
System.out
local_list: a, b, c, x ,d ,x ,c ,b, a
这时调用指令invokevirtual java/io/PrintStream/print(I)V,执行System.out对象的Print函数,把a变量的值输出到控制台。接着反复执行这几个步骤,把剩下几个变量的值依次打印到控制台上,于是上面代码中的printf语句编译成java字节码后的内容如下:

iload 0
iload 1
iload 2
iload 3
istore 5
istore 6
istore 7
istore 8
getstatic   java/lang/System/out Ljava/io/PrintStream;
ilod 8
invokevirtual   java/io/PrintStream/print(I)V
getstatic   java/lang/System/out Ljava/io/PrintStream;
ilod 7
invokevirtual   java/io/PrintStream/print(I)V
getstatic   java/lang/System/out Ljava/io/PrintStream;
ilod 6
invokevirtual   java/io/PrintStream/print(I)V
getstatic   java/lang/System/out Ljava/io/PrintStream;
ilod 5
invokevirtual   java/io/PrintStream/print(I)V

大家可以看到,一条C语言语句编译成java字节码时,编出的语句数量是原语句的十几倍,这也是为何在编译原理中,代码优化是极为重要的一环,没有优化技术,就算编出来的代码能跑,但是效率也是非常低下的。

实现上面编译功能的代码在ClibCall.java中:

private Object handlePrintfCall() {
        ArrayList<Object> argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList(false);
        String argStr = (String)argsList.get(0);
        String formatStr = "";
        
        int i = 0;
        int argCount = 1;
        String str = "";
        while (i < argStr.length()) {
            if (argStr.charAt(i) == '%' && i+1 < argStr.length() && 
                    argStr.charAt(i+1) == 'd') {
                i += 2;
                //generateJavaAssemblyForPrintf(str);
                str = "";
                formatStr += argsList.get(argCount);
                argCount++;
                //printInteger();
            } else {
                str += argStr.charAt(i);
                formatStr += argStr.charAt(i);
                i++;
            }
        }
        
        System.out.println(formatStr);
        
        generateJavaAssemblyForPrintf(argStr, argCount - 1);
        return null;
    }
    
    private void generateJavaAssemblyForPrintf(String argStr, int argCount) {
        ProgramGenerator generator = ProgramGenerator.getInstance();
        String funcName = generator.getCurrentFuncName();
        TypeSystem typeSystem = TypeSystem.getTypeSystem();
        ArrayList<Symbol> list = typeSystem.getSymbolsByScope(funcName);
        int localVariableNum = list.size();
        int count = 0;
        
        while (count < argCount) {
            generator.emit(Instruction.ISTORE, "" + (localVariableNum + count));
            count++;
        }
        
        int i = 0;
        String str = "";
        count = argCount - 1;
        while (i < argStr.length()) {
            if (argStr.charAt(i) == '%' && i+1 < argStr.length() && 
                    argStr.charAt(i+1) == 'd') {
                i += 2;
                printString(str);
                str = "";
                printInteger(localVariableNum + count);
                count--;
            } else {
                str += argStr.charAt(i);
                i++;
            }
        }
        
        printString("\n");
    }

最后,我们再看看jvm的运算指令,对整形进行四种基础运行的jvm指令为:iadd, isub, imul, idiv. 如果要计算 1+2, 那么分别把数值1和2压入堆栈,然后执行指令iadd, 那么堆栈顶部的元素就会被弹出,他们的和也就是3会被压入到堆栈,其他运算指令的原理相同。如果计算的是浮点数而不是整形,那么就得使用对应指令,他们分别是fadd, fsub, fmul, fdiv,如果计算的是长整形,那么对应的指令就是ladd, lsub, lmul, ldiv, 在我们实现的编译器中,目前暂时只支持对整形的运算指令,相应的代码实现在BinaryExecutor.java:

public class BinaryExecutor extends BaseExecutor{
    @Override
    public Object Execute(ICodeNode root) {
    ....
    case CGrammarInitializer.Binary_Plus_Binary_TO_Binary:
        case CGrammarInitializer.Binary_DivOp_Binary_TO_Binary:
        case CGrammarInitializer.Binary_Minus_Binary_TO_Binary:
        case CGrammarInitializer.Binary_Start_Binary_TO_Binary:
            //先假设是整形数相加
            int val1 = (Integer)root.getChildren().get(0).getAttribute(ICodeKey.VALUE);
            int val2 = (Integer)root.getChildren().get(1).getAttribute(ICodeKey.VALUE);
            
            if (production == CGrammarInitializer.Binary_Plus_Binary_TO_Binary) {
                String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " plus " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
                root.setAttribute(ICodeKey.VALUE, val1 + val2); 
                root.setAttribute(ICodeKey.TEXT,  text);
                System.out.println(text + " is " + (val1+val2) );   
                ProgramGenerator.getInstance().emit(Instruction.IADD);
            } else if (production ==  CGrammarInitializer.Binary_Minus_Binary_TO_Binary) {
                String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " minus " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
                root.setAttribute(ICodeKey.VALUE, val1 - val2); 
                root.setAttribute(ICodeKey.TEXT,  text);
                System.out.println(text + " is " + (val1-val2) );   
                ProgramGenerator.getInstance().emit(Instruction.ISUB);
            } else if (production ==  CGrammarInitializer.Binary_Start_Binary_TO_Binary) {
                String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " * " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
                root.setAttribute(ICodeKey.VALUE, val1 * val2); 
                root.setAttribute(ICodeKey.TEXT,  text);
                System.out.println(text + " is " + (val1 * val2) );
                
                ProgramGenerator.getInstance().emit(Instruction.IMUL);
            }
            else {
                root.setAttribute(ICodeKey.VALUE, val1 / val2); 
                System.out.println( root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " is divided by "
                        + root.getChildren().get(1).getAttribute(ICodeKey.TEXT) + " and result is " + (val1/val2) );
                ProgramGenerator.getInstance().emit(Instruction.IDIV);
            }
            
            break;
    ....
    }
}

完成本节代码后,我们的编译器能将下面C代码编译成java字节码:

int f(int a, int b, int c, int x) {
   printf("value of a, b ,c ,x is: %d, %d, %d, %d",  a, b, c, x);
   int d;
   d = (a*x*x) + (b*x);
   int e;
   int h;
   e = 6;
   h = 3;
   printf("e divided by h is : %d", e/h);
   return d;
}

void main() {
    int c; 
    c = f(1, 2, 3, 4);
    printf("result of calling f is :%d", c);
}

上面代码编译成java汇编代码的结果为:

.class public CSourceToJava
.super java/lang/Object

.method public static main([Ljava/lang/String;)V
    sipush  1
    sipush  2
    sipush  3
    sipush  4
    invokestatic    CSourceToJava/f(IIII)I
    istore  0
    iload   0
    istore  1
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "result of calling f is :"
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   1
    invokevirtual   java/io/PrintStream/print(I)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "
"
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    return
.end method
.method public static f(IIII)I
    iload   0
    iload   1
    iload   2
    iload   3
    istore  7
    istore  8
    istore  9
    istore  10
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "value of a, b ,c ,x is: "
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   10
    invokevirtual   java/io/PrintStream/print(I)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc ", "
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   9
    invokevirtual   java/io/PrintStream/print(I)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc ", "
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   8
    invokevirtual   java/io/PrintStream/print(I)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc ", "
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   7
    invokevirtual   java/io/PrintStream/print(I)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "
"
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    iload   0
    iload   3
    imul
    iload   3
    imul
    iload   1
    iload   3
    imul
    iadd
    istore  4
    sipush  6
    istore  5
    sipush  3
    istore  6
    iload   5
    iload   6
    idiv
    istore  7
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "e divided by h is : "
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   7
    invokevirtual   java/io/PrintStream/print(I)V
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "
"
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    iload   4
    ireturn
.end method

.end class

把上面java汇编代码在编译成二进制字节码运行后结果如下:


这里写图片描述

更详细的讲解和调试演示过程请参看视频。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容