这是一篇硬(闲的)核(蛋疼)的文章,我们来通过手写字节码的方式,来完成一个Helloworld。起因很简单,最近在看《深入理解JVM字节码》,尝试了解读class文件,但是总觉得不过瘾,那么我们来试试手写一个class。
工具
所谓工欲善其事必先利其器,我们先说一下工具,写字节码我用了sublime,它可以直接将文件打开成16进制查看, 并且直接进行编辑,非常方便,并且sublime允许我们在编辑的时候加入空格,来方便阅读,这个功能真的太赞了。
同时我还使用了vscode的hexdump插件,来协助。
整活
先来看一下结果,一个经典的HelloWorld程序。
Class 结构
class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。我们的Java源文件, 在被编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。可以把u1, u2, u3, u4看做class文件数据项的“类型” 。
类型 | 名称 | 介绍 | 数量 |
u4 | magic | 魔数,固定0xCAFEBABE | 1 |
u2 | minor_version | 副版本号 | 1 |
u2 | major_version | 主版本 | 1 |
u2 | constant_pool_count | 常量池计数器 | 1 |
cp_info | constant_pool | 常量池 | constant_pool_count-1 |
u2 | access_flags | 类访问标识 | 1 |
u2 | this_class | 类索引 | 1 |
u2 | super_class | 父类索引 | 1 |
u2 | interfaces_count | 接口计数器 | 1 |
interface_info | interfaces | 接口表 | interfaces_count |
u2 | field_count | 字段计数器 | 1 |
field_info | fields | 字段表 | field_count |
u2 | methods_count | 方法计数器 | 1 |
method_info | methods | 方法表 | methods_count |
u2 | attribute_count | 属性计数器 | 1 |
attribute_ino | attribute | 属性表 | attribute_count |
初建
其实构建class,数量为1的字段都非常好办,麻烦的就是几个变长部分。我们先模拟一下极简场景
magic: CAFE BABE
major_version: 0000 0034 (52 jdk8)
constant_pool: 0000
access_flags:: 0021
this_class:0000
super_class: 0000
interfaces: 0000
fields: 0000
methods: 0000
attributes: 0000
cafe babe 0000 0034 0000 0021 0000 0000 0000 0000 0000 0000
添加类名
这里需要在常量池添加2个字段,一个是CONSTANT_Class_info(标识07)存放引用,另外一个CONSTANT_Utf8_info(标识01)存放二进制对应的字符串
类型 | 标志(tag) | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 表示方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
-- Class 引用 #2
07 00 02-- UTF8 10 bytes H e l l o W o r l d
01 00 0a 48 65 6c 6c 6f 57 6f 72 6c 64
案例如下:
cafe babe 0000 0034 0003 -- 常量池 计数 30700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640021 0001 0000
0000 0000 0000 0000
添加父类
添加父类 java/lang/Object
-- Class 引用 #4
07 00 04-- UTF8 10 bytes J a v a / l a n g / O b j
01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a e c t65 63 74
案例如下:
cafe babe 0000 0034 0005 0700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740021 0001 0003
0000 0000 0000 0000
基础类完成了,我们的目标是完成下面的代码,所以还需要为常量池添加更多信息
static void main(String[] args) {
System.out.println("HelloWorld");
}
- System.out的返回类型PrintStream
- 字符串HelloWorld ,这里其实是可以和类名复用的,但是为了就结构清晰,我们还是使用新的引用
- 2个utf8类型用于映射我们的方法前面main和([Ljava/lang/String;)V
- 表示特殊属性代码的UTF8字符串。这将需要指示main方法指令的主体。
cafe babe 0000 0034000b0700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740700 06
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d0700 08
0100 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d0800 0a
0100 0b 48 65 6c 6c 6f 20 57 6f 72 6c 640021 0001 0003
0000 0000 0000 0000
添加引用字段和方法
这里主要构建System.out及其print所需相关常量池
0900 0500 0c
0c00 0d00 0e
0100 03 6f 75 74
0100 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
69 6e 74 53 74 72 65 61 6d 3b0a00 0700 10
0c00 1100 12
0100 07 70 72 69 6e 74 6c 6e
0100 15 28 4c 6a 61 76 61 2f 6c 61 6e 67
2f 53 74 72 69 6e 67 3b 29 56
cafe babe 0000 003400130700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740700 06
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d0700 08
0100 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d0800 0a
0100 0b 48 65 6c 6c 6f 20 57 6f 72 6c 640900 0500 0c
0c00 0d00 0e
0100 03 6f 75 74
0100 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
69 6e 74 53 74 72 65 61 6d 3b0a00 0700 10
0c00 1100 12
0100 07 70 72 69 6e 74 6c 6e
0100 15 28 4c 6a 61 76 61 2f 6c 61 6e 67
2f 53 74 72 69 6e 67 3b 29 560021 0001 0003
0000 0000 0000 0000
main方法
cafe babe 0000 003400160700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740700 06
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d0700 08
0100 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d0800 0a
0100 0b 48 65 6c 6c 6f 20 57 6f 72 6c 640900 0500 0c
0c00 0d00 0e
0100 03 6f 75 74
0100 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
69 6e 74 53 74 72 65 61 6d 3b0a00 0700 10
0c00 1100 12
0100 07 70 72 69 6e 74 6c 6e
0100 15 28 4c 6a 61 76 61 2f 6c 61 6e 67
2f 53 74 72 69 6e 67 3b 29 560100 04 6d 61 69 6e
0100 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e
67 2f 53 74 72 69 6e 67 3b 29 56
0100 04 43 6f 64 650021 0001 0003
0000 0000 0000 0000
添加方法体
接下来我们要为main添加实现
method_info {
2 bytes Methods access flags
2 bytes Name of method. UTF8 index in constant pool
2 bytes Type of method. UTF8 index in constant pool
2 bytes Number of attributes
* bytes Variable bytes describing attribute_info structs
}Note: the attribute we prepared for was Code. This will contain our byte code instructions.
cafe babe 0000 003400160700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740700 06
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d0700 08
0100 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d0800 0a
0100 0b 48 65 6c 6c 6f 20 57 6f 72 6c 640900 0500 0c
0c00 0d00 0e
0100 03 6f 75 74
0100 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
69 6e 74 53 74 72 65 61 6d 3b0a00 0700 10
0c00 1100 12
0100 07 70 72 69 6e 74 6c 6e
0100 15 28 4c 6a 61 76 61 2f 6c 61 6e 67
2f 53 74 72 69 6e 67 3b 29 560100 04 6d 61 69 6e
0100 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e
67 2f 53 74 72 69 6e 67 3b 29 56
0100 04 43 6f 64 650021 0001 0003
0000 000000010009 0013 0014
00000000
上面我们添加了一个空方法,接下来就就是添加打印了。
cafe babe 0000 003400160700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740700 06
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d0700 08
0100 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d0800 0a
0100 0b 48 65 6c 6c 6f 20 57 6f 72 6c 640900 0500 0c
0c00 0d00 0e
0100 03 6f 75 74
0100 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
69 6e 74 53 74 72 65 61 6d 3b0a00 0700 10
0c00 1100 12
0100 07 70 72 69 6e 74 6c 6e
0100 15 28 4c 6a 61 76 61 2f 6c 61 6e 67
2f 53 74 72 69 6e 67 3b 29 560100 04 6d 61 69 6e
0100 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e
67 2f 53 74 72 69 6e 67 3b 29 56
0100 04 43 6f 64 650021 0001 0003
0000 000000010009 0013 0014
0001
0015
0000 0015
0002 0001
0000 0009
b200 0b
1209
b600 0f
b1
0000
00000000
0009 - public static0013 0014 - main #19 ([Ljava/lang/String;) #200001 - attribute size = 1
0015 - Code Attribute ( this is index #21 in our constant pool )0000 0015 - Code Attribute size of 21 bytes. 21 bytes of code attribute:
0002 0001 - Max stack size of 2, and Max local var size of 10000 0009 - Size of code. 9 bytesThe actual machine instructions:
b200 0b - b2 = getstatic, 000b = index #11 in constant pool ( out )
1209 - 12 = ldc ( load constant ), 09 = index #19 ( Hello World )
b600 0f - b6 = invokevirtual, 000f = index #15 ( method println )
b1 - b1 = return void
0000 - Exception table of size 0
0000 - Attribute count for this attribute of 0
执行一下
到此,我们就正式完成了一个Helloworld。由于篇幅所限,未能完全展现字节码的魅力,后续也会找一些更有趣的选题跟大家分享。也欢迎大家与我交流。
完整清单
cafe babe 0000 003400160700 02
0100 0a 48 65 6c 6c 6f 57 6f 72 6c 640700 04
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 740700 06
0100 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d0700 08
0100 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d0800 0a
0100 0b 48 65 6c 6c 6f 20 57 6f 72 6c 640900 0500 0c
0c00 0d00 0e
0100 03 6f 75 74
0100 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
69 6e 74 53 74 72 65 61 6d 3b0a00 0700 10
0c00 1100 12
0100 07 70 72 69 6e 74 6c 6e
0100 15 28 4c 6a 61 76 61 2f 6c 61 6e 67
2f 53 74 72 69 6e 67 3b 29 560100 04 6d 61 69 6e
0100 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e
67 2f 53 74 72 69 6e 67 3b 29 56
0100 04 43 6f 64 650021 0001 0003
0000 000000010009 0013 0014
0001
0015
0000 0015
0002 0001
0000 0009
b200 0b
1209
b600 0f
b1
0000
00000000
参考链接:
https://medium.com/@davethomas_9528/writing-hello-world-in-java-byte-code-34f75428e0ad
https://blog.csdn.net/qq_31350373/article/details/81512856
https://blog.csdn.net/justry_deng/article/details/86079756?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.edu_weight&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.edu_weight
历史文章导读:
- 抽象语法树为什么抽象
- 基于Calcite自定义SQL解析器
- 基于JDBC实现VPD:SQL解析篇
- 如何成为一个成功的首席数据官
- 数据库深度研究(100页PPT)
- 基于Win10单机部署kubernetes应用
- 浅谈基于JDBC实现虚拟专用数据库(VPD)
如果文章对您有那么一点点帮助,我将倍感荣幸
欢迎 关注、在看、点赞、转发