从 C 编译器理解 MIPS 汇编代码

Posted

技术标签:

【中文标题】从 C 编译器理解 MIPS 汇编代码【英文标题】:Understanding MIPS assembly code from C compiler 【发布时间】:2018-10-22 20:57:59 【问题描述】:

我将 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;

我使用 MIPS gcc5.4 on the Godbolt compiler explorer 和 -O2 优化,经典 MIPS1 的默认 -march 没有负载互锁:

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 相关,但我不明白为什么它会在$L2 中的分支之前操作$5

【问题讨论】:

t = (dataptr[0] &lt;&lt; 8) + dataptr[1]; 是 16 位大端负载。但不幸的是,编译器没有使用 MIPS 加载半字指令,可能是因为数据可能不是 16 位对齐的。但不幸的是,x86-64 gcc 和 clang 也没有发现它(1 次加载 + 1 次 16 位旋转会比 2 次加载 + 移位 + 便宜):godbolt.org/g/2op8FQ 【参考方案1】:

让我们分析一下代码在做什么。一些映射使代码易于理解:

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 个操作(&gt;&gt; 1+ 1&lt;&lt; 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

编译器使用所有这些指令有效地从datalast_bytes 重新计算dataptr。实际上,如果您认为 dataptr 仅以 2 为增量从 data 提升,我们可以将其重写为:

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 循环进行计算——但是,不清楚为什么它决定这样做而不是从寄存器中选择值.也许它已经决定避免对循环结果的依赖比其他方法更快(由于指令流水线)。

【讨论】:

以上是关于从 C 编译器理解 MIPS 汇编代码的主要内容,如果未能解决你的问题,请参考以下文章

从编译器角度理解C++代码的编译链接

深入理解C

在 mips 汇编器中编写棋盘格的 bmp 文件

VC++代码的汇编分析

深入理解计算机系统(3.1)------汇编语言和机器语言

深入理解计算机系统(3.1)------汇编语言和机器语言