神秘 j8

反汇编的程序中,总能看到一堆神秘j 8,这是什么?

我被神秘 j 8 搞晕了

j 8实际上是个伪指令,会被翻译成jal x0,8,即不保存返回地址直接跳走。在判断比较后跳转中常会看见。

此外,这个j 8后面还跟了已知符号__BSS_END__来做相对标注,标识出0x8 = __BSS_END__ - 0x11bc

历史遗留问题:CoreMark与ee_printf

我们想一下 之前 移植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,s2lh 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,
// 是否要打一拍 保持PC不变
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高的。