AS汇编器源码剖析-编译一行汇编
汇编器AS读取一个汇编文件后,对文本内容处理后,会逐行进行解析。最基础的部分归结于对一行汇编的编码。assemble_one()是入口函数,实际上调用的是各处理器文件定义的md_assemble()函数。
编译一行汇编的主要任务包括3个。至于这一行汇编在整体文件中的影响和处理,比如segment,align,见后期章节解析。
1. 指令码opcode识别,得到encoding。以此为编译结果的基础。
2. 操作数的识别和encoding,encoding的结果和opcode encoding相或。
3. 特殊符的识别和encoding。encoding的结果和opcode encoding相或。
md_assemble主要调用3个函数完成了encoding任务。
解析指令码函数opcode_lookup
函数原型如下:
static templates *opcode_lookup (char**str)
主要是调用了lookup_mnemonic,查找opcodehash,得到相应的struct templates。而struct templates里包含了aarch64_opcode定义,该opcode的所有信息就都有了,encoding自然没问题。但是这里encoding只包含了opcode的域,其他的cond,operand的域都是空的,等后续步骤填充。
struct templates
{
aarch64_opcode *opcode;
struct templates *next;
};
opcode进行encoding。看起来简单,其实有个tricky的地方,就是条件码的支持。
处理条件码的支持
Arm有的指令是条件执行的,比如beq。一般的看法认为,b才是指令,而eq是条件码。但是在第三章中提到,指令码的解析是通过hash表查找的,而不是精巧的正则表达式。所以当把beq作为key的时候,是不能找到对应的’b’指令的。
当我以为大名鼎鼎的binutils as会以非常nb的方法解决这个问题的时候,发现aarch64_opcode_table里有如下代码。
这就是典型的快糙猛,as把b指令和条件码的排列组合当做指令,来了一次遍历。还好和我以前遇到的一款处理器绝大部分指令都带条件码支持不一样,arm只有少数的指令支持条件码,否则以这样快糙猛的方式,指令表的条数会达到指令数X条件码数。
所有指令码和条件码写在一起的指令,as都是以这种快糙猛方式支持的,包括ccmn,csel之类的。
对于conditioncode和opcode不在一起的指令,就没有必要这样了。因为不存在hash key没法匹配的问题。
典型的例子如新的B指令的写法,在armv8rfm的C6.6.19的说明。
很明显,新定义的B.cond指令命名才符合通常意义的“指令”,因为condition和opcode本身分开了。也不存在一个指令需要把每个condition code都要定义一次的尴尬。
解析操作数operand
主要依靠parse_operands.原型如下:
static bfd_boolean parse_operands (char*str, const aarch64_opcode *opcode)
刚才已经分析出了opcode,所以后面的operand已经有了一个格式,而parse_operands正是靠这些格式去匹配字符串,方法还是用hash表查找。
调用optional_operand_p
这个函数很简单,就是分析一些带option op的opcode,比如ret指令。
aarch64_opcode_table中定义如下:
{"ret", 0xd65f0000, 0xfffffc1f, branch_reg, 0, CORE, OP1 (Rn),QL_I1X, F_OPD0_OPT | F_DEFAULT (30)},
Arm手册C6.6.148定义如下:
分析寄存器类别操作数的encoding
可以看出,有了opcode,直接就可以针对operand进行解析。
const enumaarch64_opnd *operands = opcode->operands;
switch (operands[i])
{
caseAARCH64_OPND_Rd:
caseAARCH64_OPND_Rn:
caseAARCH64_OPND_Rm:
caseAARCH64_OPND_Rt:
caseAARCH64_OPND_Rt2:
caseAARCH64_OPND_Rs:
caseAARCH64_OPND_Ra:
caseAARCH64_OPND_Rt_SYS:
caseAARCH64_OPND_PAIRREG:
po_int_reg_or_fail (1, 0);
break;
caseAARCH64_OPND_Rd_SP:
caseAARCH64_OPND_Rn_SP:
po_int_reg_or_fail (0, 1);
break;
po_int_reg_or_fail和其他类似的函数最终是通过调用parse_reg(),根据aarch64_reg_hsh找到reg_entry,得到reg_names全局变量里定义的value,也就是regno。parse_shifter_operand本质上还是通过parse_reg()然后在做移位。
分析立即数
调用parse_constant_immediate,里面就是字符串处理了,没有什么特别的。
其他的类型的operand处理大同小异。
填充操作数encoding
解析完了就该填充了。主要函数是do_encode->aarch64_opcode_encode
主要函数为aarch64_insert_operand,根据aarch64_operands里定义的field域和parse_operands()的结果,就比较容易了
如果操作数满足operand->flags & OPD_F_HAS_INSERTER,就执行aarch64_insert_operand。前面章节讲过,绝大部分都有。
填充寄存器
通过aarch64_operands里定义的field域和parse_operands()的得到的regno,进行基本的移位和bit或操作就可以填充。参加aarch64_ins_regno,
填充立即数
aarch64_ins_imm额外需要判断(operand->flags& OPD_F_SHIFT_BY_2),决定是不是把立即数移2位。这个也可以看出来,operand的flag成员并不是芯片完全定义的,部分也是编程方便的需要。最后按照operand定义,进行基本的移位和bit或操作就可以填充。
特殊field填充
调用do_special_encoding(),典型的有F_SF和F_COND。
F_COND的情况比较简单,填充上去就可以。另外注意,arm定义的所有的condtionfield在所有的指令的位置是一样的,都是FLD_cond2。这个是代码能整齐归一化处理的基础。
if(inst->opcode->flags & F_COND)
{
insert_field (FLD_cond2, &inst->value, inst->cond->value,0);
}
F_SF的例子
idx = select_operand_for_sf_field_coding(inst->opcode);
value = (inst->operands[idx].qualifier== AARCH64_OPND_QLF_X
|| inst->operands[idx].qualifier == AARCH64_OPND_QLF_SP)? 1 : 0;
insert_field (FLD_sf, &inst->value,value, 0);
以adc指令为例看看sf的处理。
{"adc", 0x1a000000, 0x7fe0fc00, addsub_carry, 0, CORE, OP3 (Rd,Rn, Rm), QL_I3SAMER, F_SF},
select_operand_for_sf_field_coding根据class来确定idx,(Rd, Rn, Rm)的class都是AARCH64_OPND_CLASS_INT_REG,所以匹配的idx=0的结果。Qualifier在是在parse_operands()的时候获得的,既有parse_operands()直接的代码也有调用po_int_req_or_fail()得到。总之有了opcode就肯定容易获得。这里的AARCH64_OPND_QLF_X主要判断是不是64bit。这里可以看出,为什么bit31的sf不是opcode的一部分,而是和conditioncode一样的地位。
static inlineint
select_operand_for_sf_field_coding(const aarch64_opcode *opcode)
{
int idx = -1;
if (aarch64_get_operand_class(opcode->operands[0])
== AARCH64_OPND_CLASS_INT_REG)
/* normal case. */
idx = 0;
else if (aarch64_get_operand_class(opcode->operands[1])
== AARCH64_OPND_CLASS_INT_REG)
/* e.g. float2fix. */
idx = 1;
…
}
Alias的处理
Alias处理分两步
1. 调用aarch64_find_real_opcode,将alias的的索引改为原指令的在aarch64_opcode_table的index返回。在这个函数里,包含了所有的alias处理。
const aarch64_opcode *
aarch64_find_real_opcode (constaarch64_opcode *opcode)
{
/*Use the index as the key to locate the real opcode. */
intkey = opcode - aarch64_opcode_table;
intvalue;
switch (key)
{
case 3: /* ngc */
case 2: /* sbc */
value = 2; /* --> sbc. */
break;
case 5: /* ngcs */
case 4: /* sbcs */
value = 4; /* --> sbcs. */
break;
…
}
2. 对于设置了F_CONV的指令调用convert_to_real()进行转化。这时aarch64_opcode_table定义里的enum aarch64_op就起作用了。这里不针对每个alias指令进行转变,而是通过不同的op枚举来分类处理。另外发现,很多的op只在这个函数里得到试用。
static void
convert_to_real (aarch64_inst *inst, constaarch64_opcode *real)
{
const aarch64_opcode *alias = inst->opcode;
if((alias->flags & F_CONV) == 0)
goto convert_to_real_return;
switch (alias->op)
{
case OP_ASR_IMM:
case OP_LSR_IMM:
convert_sr_to_bfm (inst);
break;
caseOP_LSL_IMM:
convert_lsl_to_ubfm (inst);
break;
…
}
以lsl为例:
{"lsl", 0x53000000, 0x7f800000, bitfield, OP_LSL_IMM, CORE,OP3 (Rd, Rn, IMM), QL_SHIFT, F_ALIAS | F_P2 | F_CONV},
由于op是OP_LSL_IMM,所以调用convert_lsl_to_ubfm (inst)来处理。因为LSL和ngc与sbc之间的关系不一样。LSL并不是简单的将某个field固定,其他的和原指令相同,而是需要经过部分的逻辑处理。
而Ngc只是将sbc的某个field固定,所以无需一个特殊函数处理,就按照原指令一样的操作就足够了。所以ngc的指令定义并没有带F_CONV。
{"sbc", 0x5a000000, 0x7fe0fc00, addsub_carry, 0, CORE, OP3(Rd, Rn, Rm), QL_I3SAMER, F_HAS_ALIAS | F_SF},
{"ngc", 0x5a0003e0, 0x7fe0ffe0, addsub_carry, 0, CORE, OP2(Rd, Rm), QL_I2SAME, F_ALIAS | F_SF},
本文深入剖析AS汇编器如何处理一行汇编代码,包括指令码识别、操作数解析及编码过程。针对条件码支持、操作数识别、特殊字段填充等问题进行了详细解读。
1220

被折叠的 条评论
为什么被折叠?



