1. java是如何变成机器码的?
- 在dex2oat阶段,java生成的dex字节码经过trycompile阶段,调用code_generator_riscv64.cc,通过字节码对应的函数调用相应的汇编,汇编代码通过assembler来实现了机器码的生成。
- 在dex2oat阶段并不是所有的dex字节码都可以编译成机器码,有些字节码只可以做到优化,只能放在解释执行中取执行。现在的操作系统为了优化开机时间和打开时间,也不会过分地将字节码变成机器码,而是留到解释执行中进行runtime处理。
- 解释执行分为汇编解释执行和C++解释执行,在汇编解释执行中,字节码的实现是通过汇编指令完成的,但是此处的汇编并不是调用assembler实现的,而是工具链完成的。
2. Add指令是如何生成机器码的?
不同的汇编指令对应不同的Emit系列用来生成机器码。用最简单的系列Add系列来表示一下:
EmitR表示的是将参数通过位移和合并的方式组成一段机器码,组合的方式在riscv的官方文档里有介绍。
Assembler:
void Riscv64Assembler::EmitR(int32_t funct7, Register rs2, Register rs1, int32_t funct3, Register rd, int32_t opcode) {
uint32_t encoding = static_cast<uint32_t>(funct7) << kFunct7Shift |
static_cast<uint32_t>(rs2) << kRs2Shift |
static_cast<uint32_t>(rs1) << kRs1Shift |
static_cast<uint32_t>(funct3) << kFunct3Shift |
static_cast<uint32_t>(rd) << kRdShift |
opcode;
Emit(encoding);
}
encoding就是根据riscv的规范EmitR系列完成了机器码的生成,Emit则完成了将机器码存入buffer缓存区的工作
3. 跳转指令是如何生成机器码的?
3.1 构建测试环境
假如有如下跳转汇编指令:
beq a0,a1, 1f // beq1
add zero, zero, zero
beq a2,a3, 2f // beq2
add zero, zero, zero
beq a0,a1, 1f // beq3
add zero, zero, zero
beq a2,a3, 2f // beq4
add zero, zero, zero
1:
add zero, zero, zero
2:
add zero, zero, zero
beq a0,a1, 1b // beq5
beq a0,a1, 2b // beq6
在assembler_riscv64_test.cc的测试中为如下代码:
void BranchCondTwoRegsHelper(void (riscv64::Riscv64Assembler::*f)(riscv64::GpuRegister,
riscv64::GpuRegister,
riscv64::Riscv64Label*),
const std::string& instr_name) {
riscv64::Riscv64Label label1;// 初始化跳转类 label1
riscv64::Riscv64Label label2; // 初始化跳转类label2
(Base::GetAssembler()->*f)(riscv64::A0, riscv64::A1, &label1); // 如果a0== a1,跳转到label1
__ Add(riscv64::ZERO, riscv64::ZERO, riscv64::ZERO);
(Base::GetAssembler()->*f)(riscv64::A2, riscv64::A3, &label2);// 如果a0== a1,跳转到label2
__ Add(riscv64::ZERO, riscv64::ZERO, riscv64::ZERO);
(Base::GetAssembler()->*f)(riscv64::A0, riscv64::A1, &label1);
__ Add(riscv64::ZERO, riscv64::ZERO, riscv64::ZERO);
(Base::GetAssembler()->*f)(riscv64::A2, riscv64::A3, &label2);
__ Add(riscv64::ZERO, riscv64::ZERO, riscv64::ZERO);
__ Bind(&label1);
__ Add(riscv64::ZERO, riscv64::ZERO, riscv64::ZERO);
__ Bind(&label2);
(Base::GetAssembler()->*f)(riscv64::A0, riscv64::A1, &label1);
(Base::GetAssembler()->*f)(riscv64::A2, riscv64::A3, &label2);
std::string expected =
instr_name + " a0, a1, 1f\n" +
"add zero, zero, zero\n" +
instr_name + " a2, a3, 2f\n" +
"add zero, zero, zero\n"
instr_name + " a0, a1, 1f\n" +
"add zero, zero, zero\n" +
instr_name + " a2, a3, 2f\n" +
"add zero, zero, zero\n"+
"1:\n" +
"add zero, zero, zero\n"+
"2:\n" +
"add zero, zero, zero\n"+
instr_name + " a0, a1, 1b\n" +
instr_name + " a2, a3, 2b\n" +
DriverStr(expected, instr_name);
}
3.2 测试原理
此处编写的assembler其实就是替代了gcc的功能,assembler本身是使用c++完成的,所以不需要使用操作系统的环境。
测试的原理大概可以描述为到(Base::GetAssembler()->*f)(riscv64::A2, riscv64::A3, &label2);
这段代码为调用Assembler生成机器码存入buffer中,std::string expected
则是调用gcc工具链生成可执行文件后再反汇编,将两者的机器码进行对比,如果相等则测试成功。
接着对案例进行分析,通过assembler源码了解跳转汇编是如何生成机器码的。