如何理解这个似乎添加两个指针的基本汇编代码? [英] How to understand this basic Assembly Code that seems to be adding two pointers?

查看:54
本文介绍了如何理解这个似乎添加两个指针的基本汇编代码?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据以下 MIPS 指令构造 C 语言语句.
(var f -> $s0, 数组 A 和 B 的起始地址 -> $s6, $s7)

addi $t0, $s6, 4        //$t0 = &A[1]
add  $t1, $s6, $0       //$t1 = &A[0]
sw   $t1, 0($t0)        //A[1] = &A[0]
lw   $t0, 0($t0)        //$t0 = &A[0]
add  $s0, $t1, $t0      //f = &A[0] + &A[0]

左边是给出的说明,右边是我难以理解的评论.
我得到的最终答案是 f = &A[0] + &A[0],但这似乎不对.我做错了什么?

On the left are the instructions given and comments on the right are me struggling to understand.
Final answer I got is f = &A[0] + &A[0], but that doesn't seem right. What am I getting wrong?

推荐答案

你没疯,代码真的有那么奇怪!

添加两个指针基本上没有意义,所以这是一个棘手的问题.
等效的 C 看起来确实是错误的/疯狂的:

Adding two pointers basically never makes sense, so this is kind of a trick question.
The equivalent C does look wrong / insane:

intptr_t *A = ...;  // in $s6

 A[1] = (intptr_t)&A[0];
 f = A[1] + (intptr_t)&A[0];

请注意,有符号溢出在 C 中是未定义的行为,因此将其编译为 MIPS add 是合法的,它会捕获有符号溢出.如果我们使用 uintptr_t,所需的溢出语义将是包装/截断,而 add 没有实现.

Note that signed overflow is undefined behaviour in C, so it's legal to compile it to a MIPS add which will trap on signed overflow. If we'd used uintptr_t, the required overflow semantics would be wrapping / truncation, which add doesn't implement.

(MIPS 的真实世界 C 编译器总是使用 addu/addiu,而不是 add,即使对于有符号整数,因为未定义的行为意味着任何东西都是允许的,包括包装.如果你用 gcc -fwrapv 编译,它甚至是必需的.由于 MIPS 是 2 的补码机,addu 是相同的二进制操作添加,它的不同之处仅在于不捕获有符号溢出:当输入符号相同但输出符号不同时.)

(Real-world C compilers for MIPS always use addu / addiu, not add, even for signed int, because undefined behaviour means anything is allowed, including wrapping. It's even required if you compile with gcc -fwrapv. Since MIPS is a 2's complement machine, addu is the same binary operation as add, it differs only in not trapping on signed overflow: when the inputs are the same sign but the output has a different sign from that.)

就 C 而言,它将编译回更接近给定 asm 的东西,或者至少用 C 临时变量表​​示每个 asm 操作:

In terms of C that will compile back to something closer to the given asm, or at least represent every asm operations with a C temporary var:

我使用了 GNU C register-globalvariables 而不是函数 args,因此函数体将使用实际正确的寄存器(并且不会用额外的指令来使 asm 混乱以保存/恢复和初始化这些寄存器).所以这让我让 GCC 制作一个具有 s 寄存器作为输入和输出的 asm 块,而不是正常的调用约定.

I used GNU C register-global variables instead of function args so the function body would be using the actual correct register (and without cluttering the asm with extra instructions to save/restore and init those registers). So this lets me get GCC to make a block of asm that has s registers as inputs and outputs, instead of the normal calling convention.

#include <stdint.h>

register intptr_t *A  asm("s6");
// register char  *B  asm("s7");    // unused, no idea what type makes sense
register intptr_t f asm("s0");

void foo()
{
  volatile intptr_t *t0_ptr = A+1;  // volatile forces store and reload
  intptr_t t1 = (intptr_t)A;

  *t0_ptr = t1;                  //sw   $t1, 0($t0)       //A[1] = &A[0]
  intptr_t t0_int = *t0_ptr;     //lw   $t0, 0($t0)       //$t0 = &A[0]
  f = t0_int + t1;               //add  $s0, $t1, $t0     //f = &A[0] + &A[0]
  //return f;
}

请注意,$t0 在这里用于 2 种不同的事物,具有不同的类型:一种是指向数组的指针,另一种是数组中的值.我用两个不同的 C 变量表达了这一点,因为事情通常是这样进行的.(当一个变量在/之前死"时,编译器将为不同的变量重用相同的寄存器,因为需要另一个变量.)

Note that $t0 gets used for 2 different things here, with different types: one being a pointer into the array, and the other a value from the array. I expressed this with two different C variables, because that's how things normally go. (Compilers will reuse the same register for a different variable when one is "dead" before / as the other one is needed.)

GCC5.4 for MIPS 的结果汇编,带有 使 MARS 兼容的 asm 的选项:-O2 -march=mips3 -fno-delayed-branch.MIPS3 意味着没有加载延迟槽,就像问题中的代码在加载后的指令中使用 lw 结果一样.(Godbolt 编译器浏览器)

The resulting asm from GCC5.4 for MIPS, with options to make MARS-compatible asm: -O2 -march=mips3 -fno-delayed-branch. MIPS3 means no load delay slots, like the code in the question which uses the lw result in the instruction after the load. (Godbolt compiler explorer)

foo:
        move    $2,$22         # $v0, $s6   pointless copy into $v0
        sw      $22,4($2)      # A[1] = A
        lw      $3,4($22)      # v1 = A[1]
        addu    $16,$22,$3     # $s6 = (intptr_t)A + A[1]
        j       $31
        nop                                  # branch-delay slot

(GCC 使用数字寄存器名称,而不是像 $s? 用于调用保留的 ABI 名称,$t? 用于调用破坏的暂存寄存器等.http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch05s03.html 有一张桌子.)

(GCC uses numeric register names, not the ABI names like $s? for call-preserved, $t? for call-clobbered scratch regs, etc. http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch05s03.html has a table.)

另一种不那么严谨的编写方式:重要的区别是缺少 volatile 来强制编译器重新加载.

Another way to write it, with less rigour: the important difference is the lack of volatile to force the compiler to reload.

void bar() {
  A[1] = &A[0];
  f = A[1] + (intptr_t)&A[0];
}

bar:
        move    $2,$22          # still a useless copy
        sw      $22,4($2)
        sll     $16,$22,1       # 2 * (intptr_t)A;   no reload, just CSE the store value.
        j       $31
        nop


当然还有其他表达方式,例如使用 A 作为指针数组而不是 intptr_tintint32_t 数组.


Of course there would be other ways to express this, e.g. using A as an array of pointers instead of an array of intptr_t, int, or int32_t.

我选择整数是因为当你做指针加法时,C 指针类型会神奇地按类型宽度进行缩放.

I chose integers because C pointer types magically scale by the type width when you do pointer addition.

这篇关于如何理解这个似乎添加两个指针的基本汇编代码?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆