科普LLVM中MSP430如何lower移位指令

来源:互联网 发布:冰锐和锐澳的区别 知乎 编辑:程序博客网 时间:2024/05/21 22:59

问题背景

最近在做LLVM项目的时候,碰到了一个看起来不是问题的问题,就是如何对移位指令进行lower,在LLVMIR中,移位指令使用一个节点进行表示的,表示如下

(shl a, b)

其中,假设a就是源操作数,b是移位次数,shl为逻辑左移,可是MSP430芯片里面只有一次移动一位的移位指令,如下

(MSP430shl a)

表示将源操作数a向左移动一位,那么下一步,顺理成章地就会想到,如果每次只移动一位,那么肯定要使用循环进行实现啊,没错,可是问题就在这里,如何用循环实现。

编译器背景知识

基本块

所谓的基本块,比如在C语言中一段代码要么全部执行,要么全部不执行,比如如下代码

if(a == 1) {    b = b + 1;    for(int c = 0; c < 1; c ++) {        b = b + c;    }    a = b;}

这是我随便写的一段代码,里面就包含三个基本块,分别是

b = b + 1
for(int c = 0; c < 1; c ++) {    b = b + c;}
a = b;

DAG

DAG就是有向无环图,每个基本块在进行分析的时候都是变成一个有向无环图进行处理的,是一一对应关系,简单的例子就是数据结构里面的有向无环图的举例,那就是一个简单的语法树。

解决问题

在上一个章节说过,一个基本块是对应一个DAG的,然而如果你要处理第一个部分所说的移位指令,那么你就要生成一个循环,一个循环。。一个循环。。。。
问题来了,循环内部是一个基本块,也就是说,循环的出现使得整个基本块变成了三个,从而基本块和DAG的关系不满足一对一,而是三对一了,这显然是不能够处理的,但是,办法总是有的,办法就是使用伪指令匹配

  • 如果碰到了移位指令,比如上面的逻辑左移,那么就只是简单的替换一下名称,源码的出现位置如下:
SDValue MSP430TargetLowering::LowerShifts(SDValue Op,                                          SelectionDAG &DAG) const {  unsigned Opc = Op.getOpcode();  SDNode* N = Op.getNode();  EVT VT = Op.getValueType();  SDLoc dl(N);  // Expand non-constant shifts to loops:  if (!isa<ConstantSDNode>(N->getOperand(1)))    switch (Opc) {    default: llvm_unreachable("Invalid shift opcode!");    case ISD::SHL:      return DAG.getNode(MSP430ISD::SHL, dl,                         VT, N->getOperand(0), N->getOperand(1));    case ISD::SRA:      return DAG.getNode(MSP430ISD::SRA, dl,                         VT, N->getOperand(0), N->getOperand(1));    case ISD::SRL:      return DAG.getNode(MSP430ISD::SRL, dl,                         VT, N->getOperand(0), N->getOperand(1));    }  uint64_t ShiftAmount = cast<ConstantSDNode>(N->getOperand(1))->getZExtValue();  // Expand the stuff into sequence of shifts.  // FIXME: for some shift amounts this might be done better!  // E.g.: foo >> (8 + N) => sxt(swpb(foo)) >> N  SDValue Victim = N->getOperand(0);  if (Opc == ISD::SRL && ShiftAmount) {    // Emit a special goodness here:    // srl A, 1 => clrc; rrc A    Victim = DAG.getNode(MSP430ISD::RRC, dl, VT, Victim);    ShiftAmount -= 1;  }  while (ShiftAmount--)    Victim = DAG.getNode((Opc == ISD::SHL ? MSP430ISD::RLA : MSP430ISD::RRA),                         dl, VT, Victim);  return Victim;}

可以看到就是switch中的第一个case

case ISD::SHL:      return DAG.getNode(MSP430ISD::SHL, dl,                         VT, N->getOperand(0), N->getOperand(1));

变量N就是原始的移位操作节点,操作符是ISD::SHL,这里取第0个操作数N->getOperand(0)和第1个操作数N->getOperand(1),使用MSP430ISD::SHL进行节点的构造,可以看出,这里仅仅只是替换了一个名字而已,那么MSP430ISD::SHL这个节点在哪里定义的呢?答案是在td文件里面定义的,存在td文件MSP430InstrInfo.td,定义处如下

def MSP430shl     : SDNode<"MSP430ISD::SHL", SDT_MSP430Shift, []>;
  • 此时我们已经用MSP430ISD::SHL替换ISD::SHL了,对于这条移位指令来说,整个lower阶段就已经结束了,接下来进入指令选择阶段,要开始进行DAG Covering了,同样,在td文件中,我们可以发现如下的匹配规则
def Shl8     : Pseudo<(outs GR8:$dst), (ins GR8:$src, GR8:$cnt),                        "# Shl8 PSEUDO",                        [(set GR8:$dst, (MSP430shl GR8:$src, GR8:$cnt))]>;

这条匹配规则的含义就是用MSP430shl操作对srccnt两个操作数进行运算,运算的结果放在dst寄存器里面,ps.这个格式是Lisp风格的,很适合表示一个图结构,然后整个操作匹配成了一条伪指令,至于什么是伪指令,其实有一种很好理解的方式就是,伪指令也是一种算数运算,这种运算过程可能有很多步,但是对于之后的指令,如果要使用这条伪指令的结果,尽管使用,不用管这个值是怎么求出来的,有一种面向对象的特性“封装”的既视感。

  • 用伪指令进行指令选择之后,整个DAG图变成了一个指令序列,相当于是DAG -> List<Instruction>,此时还有最后一个处理步骤就是伪指令扩展,源码中有两个位置处理伪指令扩展,一个是td文件里面
let usesCustomInserter = 1 in {  def Select8  : Pseudo<(outs GR8:$dst), (ins GR8:$src, GR8:$src2, i8imm:$cc),                        "# Select8 PSEUDO",                        [(set GR8:$dst,                          (MSP430selectcc GR8:$src, GR8:$src2, imm:$cc))]>;  def Select16 : Pseudo<(outs GR16:$dst), (ins GR16:$src, GR16:$src2, i8imm:$cc),                        "# Select16 PSEUDO",                        [(set GR16:$dst,                          (MSP430selectcc GR16:$src, GR16:$src2, imm:$cc))]>;  let Defs = [SR] in {  def Shl8     : Pseudo<(outs GR8:$dst), (ins GR8:$src, GR8:$cnt),                        "# Shl8 PSEUDO",                        [(set GR8:$dst, (MSP430shl GR8:$src, GR8:$cnt))]>;  def Shl16    : Pseudo<(outs GR16:$dst), (ins GR16:$src, GR8:$cnt),                        "# Shl16 PSEUDO",                        [(set GR16:$dst, (MSP430shl GR16:$src, GR8:$cnt))]>;  def Sra8     : Pseudo<(outs GR8:$dst), (ins GR8:$src, GR8:$cnt),                        "# Sra8 PSEUDO",                        [(set GR8:$dst, (MSP430sra GR8:$src, GR8:$cnt))]>;  def Sra16    : Pseudo<(outs GR16:$dst), (ins GR16:$src, GR8:$cnt),                        "# Sra16 PSEUDO",                        [(set GR16:$dst, (MSP430sra GR16:$src, GR8:$cnt))]>;  def Srl8     : Pseudo<(outs GR8:$dst), (ins GR8:$src, GR8:$cnt),                        "# Srl8 PSEUDO",                        [(set GR8:$dst, (MSP430srl GR8:$src, GR8:$cnt))]>;  def Srl16    : Pseudo<(outs GR16:$dst), (ins GR16:$src, GR8:$cnt),                        "# Srl16 PSEUDO",                        [(set GR16:$dst, (MSP430srl GR16:$src, GR8:$cnt))]>;  }}

内容如上,对于要进行伪指令扩展的指令,需要声明usesCustomInserter = 1。C++中的源码如下:

MachineBasicBlock*MSP430TargetLowering::EmitInstrWithCustomInserter(MachineInstr *MI,                                                  MachineBasicBlock *BB) const {  unsigned Opc = MI->getOpcode();  if (Opc == MSP430::Shl8 || Opc == MSP430::Shl16 ||      Opc == MSP430::Sra8 || Opc == MSP430::Sra16 ||      Opc == MSP430::Srl8 || Opc == MSP430::Srl16)    return EmitShiftInstr(MI, BB);    ......}

其中,这里的

MachineBasicBlock*MSP430TargetLowering::EmitInstrWithCustomInserter(MachineInstr *MI,                                                  MachineBasicBlock *BB) const;

只一个纯虚函数,需要由子类进行实现,这里就由MSP430平台进行了实现,可以看到,在函数的开头,就进行了判断,如果是移位指令就转到EmitShiftInstr函数体内进行处理。EmitShiftInstr函数我就补贴代码了,下面就直接用图和代码进行说明,对照图,代码就很好看懂。
存在伪指令节点

(MSP430shl GR8:$src, GR8:$cnt)

最终生成的汇编码自己手写如下

//////                // Previous Part : Part Aloop: shl src         // 将src左移一位      dec cnt         // 移位次数减一      eq  cnt, 0      // 如果移位次数为0,那么就跳过下一条语句,反之不跳过      jmp loop        // 跳转指令,跳转到loop继续执行/////                 // Remain Part : Part B

反映在实际操作中就是手撸DAG图,对其进行改造
伪指令节点DAG:
这里写图片描述
最终变换完成的DAG图如下:

其中,中间的大节点中的指令是硬编码的,然后再在中间的大节点的开头插入phi函数,完成移位指令的lower。

结语

细心的你们会发现为什么中间的大节点和后面的jmp节点前后为什么会有留白区域,很简单,这只是贫僧画图水平不高,做了两个标记,不知道怎么去掉而已,ps,画图工具是Graphviz。

原创粉丝点击