c - 从 C 编译器理解 MIPS 汇编代码
问题描述
我将 C 代码转换为 MIPS,但我无法理解 MIPS 指令的一部分:
#include <inttypes.h>
#include <stdint.h>
uint16_t
chksum(uint16_t sum, const uint8_t *data, uint16_t len)
{
uint16_t t;
const uint8_t *dataptr;
const uint8_t *last_byte;
dataptr = data;
last_byte = data + len - 1;
while (dataptr < last_byte)
{
t = (dataptr[0] << 8) + dataptr[1];
sum += t;
if (sum < t)
{
sum++;
}
dataptr += 2;
}
if (dataptr == last_byte)
{
t = (dataptr[0] << 8) + 0;
sum += t;
if (sum < t)
{
sum++;
}
}
return sum;
}
我在 Godbolt 编译器资源管理器上使用 MIPS gcc5.4 进行优化-O2
,默认-march
经典 MIPS1 没有加载互锁:
chksum(unsigned short, unsigned char const*, unsigned short):
andi $6,$6,0xffff
addiu $6,$6,-1
addu $6,$5,$6
sltu $3,$5,$6
beq $3,$0,$L2
andi $2,$4,0xffff
move $4,$5
$L4:
lbu $3,0($4)
lbu $7,1($4)
sll $3,$3,8
addu $3,$3,$7
andi $3,$3,0xffff
addu $2,$3,$2
andi $2,$2,0xffff
addiu $4,$4,2
sltu $3,$2,$3
sltu $7,$4,$6
beq $3,$0,$L3
addiu $8,$2,1
andi $2,$8,0xffff
$L3:
bne $7,$0,$L4
nor $3,$0,$5
addu $3,$3,$6
srl $3,$3,1
addiu $3,$3,1
sll $3,$3,1
addu $5,$5,$3
$L2:
beq $6,$5,$L8
nop
$L9:
j $31
nop
$L8:
lbu $3,0($6)
nop
sll $3,$3,8
addu $2,$3,$2
andi $2,$2,0xffff
sltu $3,$2,$3
beq $3,$0,$L9
nop
addiu $2,$2,1
j $31
andi $2,$2,0xffff
我将大部分指令与代码匹配,但直到之前我无法理解$L3
从指令开始的部分。nor
addu
$L2
while
Compiler Explorer 显示该部分$5
与$L2
.
解决方案
让我们分析一下代码在做什么。一些映射使代码易于遵循:
Initial parameters:
$4: sum parameter
$5: data parameter
$6: len parameter
Labels:
$L4: while body
$L3: while condition
$L2: if condition
Registers:
$2: sum
$4: dataptr
$6: last_byte
相关代码:
// [...]
sltu $3,$5,$6 // $3 = $5 (data parameter) < $6 (last_byte) ? 1 : 0
beq $3,$0,$L2 // if $3 == 0 goto $L2 (if condition)
andi $2,$4,0xffff // $2 (sum) = $4 (sum parameter) & 0xFFFF
move $4,$5 // $4 (dataptr) = $5 (data parameter)
$L4: // while body
// [...]
sltu $7,$4,$6 // $7 = $4 (dataptr) < $6 (last_byte) ? 1 : 0
// [...]
$L3: // while condition
bne $7,$0,$L4 // if $7 != 0 goto $L4 (while body) [1]
nor $3,$0,$5 // $3 = $5 (data) nor 0
addu $3,$3,$6 // $3 += $6 (last_byte)
srl $3,$3,1 // $3 >>= 1
addiu $3,$3,1 // $3++
sll $3,$3,1 // $3 <<= 1
addu $5,$5,$3 // $5 += $3
$L2: // if condition
beq $6,$5,$L8 // if $6 (last_byte) == $5 goto $L8 [2]
while
循环结束[1]
于。其余的指令直到[2]
将一个值计算到寄存器中以与源代码中的( )$5
进行比较。$6
last_byte
if
这里的问题是: 中的值是$5
多少?如果你把所有的操作放在一起,你会得到:
$5 = $5 + ((((($5 nor 0) + $6) >> 1) + 1) << 1)
让我们解开这个表达式。首先,意识到:
x NOR 0 = NOT(x OR 0) = ~(x | 0) = ~x
所以它只是在 上否定(一个补码)$5
。
然后,它添加$6
,即last_byte
。
接下来的 3 个操作 ( >> 1
, + 1
, << 1
) 是一种计算下一个偶数整数的方法。看看一些情况会发生什么:
0000 (0) -> 0010 (2)
0001 (1) -> 0010 (2)
0010 (2) -> 0100 (4)
0011 (3) -> 0100 (4)
0100 (4) -> 0110 (6)
0101 (5) -> 0110 (6)
0110 (6) -> 1000 (8)
0111 (7) -> 1000 (8)
最后,它添加了 的原始值$5
,也就是data
参数。
如果将所有内容放在一起,并为清楚起见替换为 C 变量的名称,您会得到:
$5 = data + next_even(~data + last_byte)
回想一下,对于二进制补码整数:
x - y == x + ~y + 1
所以:
$5 = data + next_even(last_byte - data - 1)
= data + next_even(len - 2)
现在,计算减法后的下一个偶数,2
基本上就是去掉最低位的信息;换句话说,是偶数的“地板”。这可以表示为如果它是偶数则返回相同的数字,或者如果它是奇数则少一个,即:
$5 = data + (len % 2 ? len : len - 1)
最后,编译器将此寄存器与$6
( last_byte
) 进行比较。简化:
last_byte == data + (len % 2 ? len : len - 1)
data + len - 1 == data + (len % 2 ? len : len - 1)
len - 1 == len % 2 ? len : len - 1
len % 2 != 0
现在我们还可以看到,表达式实际上只依赖于len
,而不依赖于data
。
带有所有这些指令的编译器有效地dataptr
从data
和重新计算last_bytes
。事实上,如果你认为dataptr
它只data
是以 为增量2
,我们可以将其重写为:
data + 2 * n_increments
data + 2 * (len / 2)
data + (len % 2 ? len : len - 1)
这正是$5
上面计算的值。
知道了这一点,人们就会想知道为什么编译器会得出这个解决方案。最新版本的 GCC (8.1.0) 和 x86-64 也会发生同样的情况:
mov rdx, rsi
not rdx
add rdx, r8
shr rdx
lea rsi, [rsi+2+rdx*2]
很明显,优化器意识到 的最终值dataptr
可以独立于while
循环计算——但是,不清楚为什么它决定这样做而不是从寄存器中选择值。也许它已经决定避免对循环结果的依赖比其他方式更快(由于指令流水线)。
推荐阅读
- mysql - Docker MySQL:通信链路故障
- garbage-collection - 在启用 DisableExplicitGC 的情况下安排 FullGC
- javascript - 在移动设备上调整大小时的多步骤表单
- python - Azure函数python - 如何防止SNAT端口耗尽?
- database - Firebase 的限制
- java - 某些扩展的 Java 路径过滤
- node.js - 在 Electron 桌面应用程序中创建 TLS 客户端
- android - 如何在focusArea周围添加Shadow Over TextureView
- javascript - Replace data from Array based on the Id
- mysql - 在 heroku "connect ECONNREFUSED 127.0.0.1:3306" 上部署我的 api 后出现此错误