步骤一:对跳转指令进行占位
##1. 跳转指令是没有办法直接生成机器码的
beq a0 a1 1f
该指令的意思是如果a0寄存器的值等于a1寄存器,那么就跳转到向下1:
处的地址。对于机器来说,机器暂时还不知道1f
在哪,因为尚未执行到1f
,这是一个未知的地址。在这种情况下,beq暂时还没办法生成机器码。
那么如何处理呢?
先做标记,然后占位。由于并不知道要跳往哪里,先把变量目的地址target设置为一个极大值,然后标记,标记是指记录下此时beq的地址,位于buffer的哪里,并且占据beq所需要的长度。对于beq来说,beq不是伪指令,所需要的空间只是一个四字节,因为beq最后调用EmitB生成的机器码encoding其实就是一个int类型的数,需要四字节。但如果指令是伪指令,就需要占据伪指令长度所需要的长度比如call是伪指令,实际上通过两条在指令实现的,就需要八字节空间。
##2. 源码分析:
(Base::GetAssembler()->*f)(riscv64::A0, riscv64::A1, &label1);
调用的是assembler_riscv64.cc中的Bond来实现标记和占位操作
void Riscv64Assembler::Bcond(Riscv64Label* label,
BranchCondition condition,
GpuRegister lhs,
GpuRegister rhs) {
if (Branch::IsNop(condition, lhs, rhs)) {
return;
}
uint32_t target = label->IsBound() ? GetLabelLocation(label) : Branch::kUnresolved;
// beq要跳往的地址 判断是往前还是往后
branches_.emplace_back(buffer_.Size(), target, condition, lhs, rhs);
// beq1 调用了Branch类的构造函数生成了对象,塞入容器中,目前只有一个元素,就是beq1的对象,对象中包含了成员变量,size = 1, branches_[0]
// beq2 branches的第二个元素,容器来可以访问beq2的location,target
FinalizeLabeledBranch(label);
// beq1 在容器中塞入了对象,并在buffer中属于beq1的位置进行了占位,且用的是beq1的pos进行占位的,pos = 8
// beq2 在buffer中用8进行了占位,同时linkto pos = 9
}
2.1 设置跳转目标地址target
uint32_t target = label->IsBound() ? GetLabelLocation(label) : Branch::kUnresolved;
bool IsBound() const { return position_ < 0; }
跳转到同一个地址的跳转指令公用一个label类,比如beq1 和beq3使用的是一个label1,但beq2 beq4跳转的地址是2f,所以使用的一个新的label类。
此时beq1调用Bcond,label1被初始化,成员变量还是初始值,默认的position为0,所以target= Branch::kUnresolved
,kUnresolved的值为0xffffffff,为一个很大的值,等执行到Bind(label1)之后才能知道target的值为多少。
position
这个变量的正负用来判断是往前’1b’还是往后1f
跳转,因为如果是往前跳转的话,可以直接计算出target的值。至于position变量值的作用后文再作详细介绍。
2.2 将beq1生成的对象存入容器
branches_.emplace_back(buffer_.Size(), target, condition, lhs, rhs);
将beq1生成对象存入容器中。
在通常情况下,容器的类型均为int类型,作用与数组相似,是一种可变大小的数组。
此处容器的定义为类容器数组: std::vector<Branch> branches_[];
,给容器推入参数之后,会调用类的构造函数生成对象
// Conditional branch.
Branch(uint32_t location,
uint32_t target,
BranchCondition condition,
GpuRegister lhs_reg,
GpuRegister rhs_reg);
此处的buffer_.Size()表示buffer的大小,此时的大小就是beq1的location。容器branches_.size() = 1。对于对象来说,是由成员变量组成的,因为成员函数此时是不占地址的,可以通过beq1生成的对象访问成员变量,比如branches[0].location_ = 0
当程序运行到(Base::GetAssembler()->*f)(riscv64::A2, riscv64::A3, &label2);
会继续往容器branches_存入beq2生成的对象,branches_.size() = 2, branches[1].location_ = 8
2.3 对跳转指令进行占位
Bcond中最重要的函数为FinalizeLabeledBranch(label)
,来看其实现:
void Riscv64Assembler::FinalizeLabeledBranch(Riscv64Label* label) {
uint32_t length = branches_.back().GetLength();
// length是跳转指令的长度,如果是beq 1,伪指令, 占位 有多少len 占位多少 占位
if (!label->IsBound()) { // pos的正负就是判断往前还是往后 初始化 为0 pos
// Branch forward (to a following label), distance is unknown.
// The first branch forward will contain 0, serving as the terminator of
// the list of forward-reaching branches.
Emit(label->position_);// 通过强制类型转换,用pos进行占位,beq1 0 进行占位 所有属于一个label的对象用链表的方式
length--; // 已经用pos占位四字节,len--
// Now make the label object point to this branch
// (this forms a linked list of branches preceding this label).
uint32_t branch_id = branches_.size() - 1;
label->LinkTo(branch_id);//pos 表示的是beq在容器中的位置
}
// Reserve space for the branch.
for (; length != 0u; --length) {
Nop(); // 这种占位,只会用0占位,只负责占位
}
}
对于往前跳转的如beq5,不需要进行额外的操作,因为Bind(label1)的工作在beq5之前就已经完成了,意味着每次跳转到该label1的跳转指令都可以知道所要跳转的目标地址,target可以直接计算出。占位就可以直接使用Nop这种没有特殊意义的站位进行占位。
但是对于向后跳转的指令来说,假如执行到Bind(label1)的时候如何区分容器中的哪些对象是属于label1而不是属于label2的呢?因为容器只有一个容器。
Emit(label->position_);
解决了这个问题来看其代码:
void Riscv64Assembler::Emit(uint32_t value) {
...
// Other instructions are simply appended at the end here.
AssemblerBuffer::EnsureCapacity ensured(&buffer_);
buffer_.Emit<uint32_t>(value);
}
}
template<typename T> void Emit(T value) {
CHECK(HasEnsuredCapacity());
*reinterpret_cast<T*>(cursor_) = value; // 指针指向的是四字节内容,内容中是0,beq要的四字节,初始化为0, cursor buffer中的最后一
cursor_ += sizeof(T);
}
label->position_是Emit的入参,当beq1调用该函数时,label1成员变量position_尚未被修改还是初始值为0, *reinterpret_cast<T*>(cursor_) = value
中指针cursor_
始终指向buffer的当前地址,也就是末尾地址。但指针本身指向的一个8位的内容,此时通过reinterpret_cast
进行强制类型转化,并调用模板函数,变成4字节32位的内容,这个内容在beq1时位0。之后再将光标cursor_
加上int的大小,继续指向buffer的末尾。其实此时beq1的占位工作已经完成,是通过强制类型转化,用pos占据了beq所需要的的四字节内容。
接着beq1继续执行
uint32_t branch_id = branches_.size() - 1;
此时容器的大小为1,branch_id = 0
label->LinkTo(branch_id); position_ = position + sizeof(void*);
在LinkTo阶段,将position与branch_id进行绑定,此时label1->postion = 0 + 8 = 8
此时如果属于label2的beq2进入Emit(label->position_);
此时label2被初始化,pos还是0,所以依然是使用0进行占位,但是此时容器中已经有两个对象,pos= branches_.size() - 1 + 8 = 2-1+8 = 9
接着属于label1的beq3进入Emit(label->position_);
,此时labe1->position = 8,使用6进行占位,此时容器中有三个对象,pos= 3 - 1 + 8 = 10
接着属于label2的beq4执行到Emit(label->position_);
,此时labe2->position = 9,使用9进行占位,此时容器中有四个对象,pos= 4 - 1 + 8 = 11
此时跳转指令的占位逻辑已经很清楚了,对于向前跳转的跳转指令,直接使用Nop进行占位,对于向后跳转的指令,跳转指令的占位是使用同属于该label的上一条跳转对象的pos,该pos记录着在容器中的位置。