神秘 j8
反汇编的程序中,总能看到一堆神秘j 8,这是什么?
j 8实际上是个伪指令,会被翻译成jal x0,8,即不保存返回地址直接跳走。在判断比较后跳转中常会看见。
此外,这个j 8后面还跟了已知符号__BSS_END__来做相对标注,标识出0x8 = __BSS_END__ - 0x11bc。
我们想一下 之前 移植CoreMark后遇到的问题:在每次的crc更新后,必须插入一句ee_printf。这个问题我们还没有解决。
反汇编分析
这个问题可能出现在哪?我们回过头看一下原先出问题的C代码:
1 2 3 4 seedcrc = crc16(results[0 ].seed1, seedcrc); seedcrc = crc16(results[0 ].seed2, seedcrc); seedcrc = crc16(results[0 ].seed3, seedcrc); seedcrc = crc16(results[0 ].size, seedcrc);
注意到,这是函数的连续调用。连续调用很可能在寄存器返回值 、紧邻的下一次调用参数装载 或 jal/jalr 路径上踩到了 CPU 的相关 bug。让我们看看反汇编出来的是什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 # 未插入 ee_printf | # 插入 ee_printf # 相同的开头 jal 2998 <get_time> | jal 29c0 <get_time> mv s4,a0 | mv s4,a0 lh a0,2028(s0) | lh a0,2028(s0) mv s6,a1 | mv s6,a1 li a1,0 | li a1,0 jal 2710 <crc16> | jal 2738 <crc16> # 第一次CRC16后 mv a1,a0 | lui s1,0x3 lh a0,2030(s0) | mv s2,a0 jal 2710 <crc16> | addi a0,s1,1296 # ee_printf 调用 | jal 2a30 <ee_printf> | lh a0,2030(s0) | mv a1,s2 | jal 2738 <crc16> # 第二次CRC16后 mv a1,a0 | mv s2,a0 lh a0,2032(s0) | addi a0,s1,1296 jal 2710 <crc16> | jal 2a30 <ee_printf> | lh a0,2032(s0) | mv a1,s2 | jal 2738 <crc16> # 第三次CRC16后 mv a1,a0 | mv s2,a0 lh a0,52(sp) | addi a0,s1,1296 jal 2710 <crc16> | jal 2a30 <ee_printf> | lh a0,52(sp) | mv a1,s2 | jal 2738 <crc16> # 第四次CRC16后 相同的结尾 lui a5,0x8 | lui a5,0x8 addi a5,a5,-1275 | addi a5,a5,-1275 mv s5,a0 | mv s5,a0 beq a0,a5,1110 <main+0x778> | beq a0,a5,1138 <main+0x7a0>
很明显,在插入ee_printf之前,调用crc16的函数跳转入口jal前一条都是lh a0, xxx,再前一条是mv a1,a0,两条指令间存在数据依赖;插入后,跳转函数入口前变成了mv a1,s2和lh a0, xxx,数据依赖消失了!
由此,我们可以定位到问题:一定是竞争冒险的地方存在一些问题,导致原先的跳转/重定向/保存数据被吃掉了!
俺寻思 PC
跳转是从哪里开始的?是PC寄存器。PC寄存器的值是谁给的?是程序计数器模块 PC.sv 。我们再
我们回过头,再看一下:
1 2 3 4 5 always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) pc_if <= `INITIAL_PC; else if (keep_pc) pc_if <= pc_if; else pc_if <= npc; end
这里存在什么问题?keep_pc的优先级高于take_branch。因此,当EX级的jal决定跳转时,如果MEM级的lh正在产出a0,而ID级的mv a1,a0正在读取,会判断成Load-Use冒险,拉高keep_pc。一旦keep_pc=1,本次jal的PC重定向就会被吃掉。
而插入ee_printf后,jal crc16后面紧挨着的错误路径指令指令不再是 mv a1,a0,而是 lui 之类不读 a0 的指令,同时编译器把 crc16 返回值先存到 s2内,jal生效那一拍通常不会再把 keep_pc拉高,看起来问题被解决了。
因此修改一下PC模块,再精简一下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 `include "include /defines.svh" module PC ( input logic clk, input logic rst_n, input logic keep_pc, input logic branch_op, input logic [31 :0 ] branch_target, output logic [31 :0 ] pc_if, output logic [31 :0 ] pc4_if ); assign pc4_if = pc_if + 4 ; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) pc_if <= `INITIAL_PC; else if (branch_op) pc_if <= branch_target; else if (keep_pc) pc_if <= pc_if; else pc_if <= pc4_if; end endmodule
运行一下,一切正常。
我们再确认一下其他的流水线寄存器中是否存在问题,如IF/ID级流水线寄存器:
1 2 3 4 5 6 7 8 9 10 11 always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin end else if (flush) begin end else if (stall) begin end else begin end end
可以看出,flush的优先级确实是比stall高的。