Assembler如何把跳转汇编变成机器码的(二)

本文详细介绍了RISC-V汇编器处理跳转指令的过程,包括对向后跳转指令的占位策略。通过占位标记和后续的地址绑定,确保了正确的目标地址计算。在占位时,使用了极大值作为临时目标地址,并通过容器存储相关信息,最终通过FinalizeLabeledBranch函数完成占位。这一过程对于理解编译器内部工作原理和指令编码机制具有重要意义。
摘要由CSDN通过智能技术生成

步骤一:对跳转指令进行占位

使用占位符对跳转指令进行占位
##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记录着在容器中的位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值