mips 汇编函数,结构应该是什么(交换示例)

mips assembly functions, what should be the structure (example swap)

所以我是 mips 汇编的初学者,我知道如何使用 lw、sw、addi 等基本指令,但我无法真正理解这些指令如何组合在一起形成一个函数。 如果假设我想在 mips 中编写一个简单的交换函数,那么指令的顺序应该如何才能使代码按预期工作?

是否有关于应该如何编写函数的通用“规则”?我先做什么,然后再做什么? 另外,计算机内存如何与寄存器交互?

正如我提到的交换,下面是我发现的一个例子,我不明白为什么每一行对函数“有用”,尤其是当涉及到(我认为)存储向量( ?)

如果至少有人能解释第 5-10 行(从 sll 开始),那将真正有帮助。

抱歉一下子问了这么多问题,我真的很困惑。

swap:               #swap method

    addi $sp, $sp, -12  # Make stack room for three

    sw $a0, 0($sp)      # Store a0
    sw $a1, 4($sp)      # Store a1
    sw $a2, 8($sp)      # store a2

    sll $t1, $a1, 2     #t1 = 4a
    add $t1, $a0, $t1   #t1 = arr + 4a
    lw $s3, 0($t1)      #s3  t = array[a]

    sll $t2, $a2, 2     #t2 = 4b
    add $t2, $a0, $t2   #t2 = arr + 4b
    lw $s4, 0($t2)      #s4 = arr[b]

    sw $s4, 0($t1)      #arr[a] = arr[b]
    sw $s3, 0($t2)      #arr[b] = t 


    addi $sp, $sp, 12   #Restoring the stack size
    jr $ra          #jump back to the caller
    
 

Is there a general "rule" about how a function should be written? What do I do first and then what do I do after that? Also how does the computer memory interact with the registers?


是的,主要是 — 要在汇编中编写一个函数,我们这样写:

  1. 入口点——函数标签
  2. 函数序言——堆栈分配和寄存器保存(需要时)
  3. 函数体——你的函数的算法
  4. 函数结尾——寄存器恢复和堆栈释放(需要时)
  5. 最后,转return转给来电者

该函数必须遵循调用约定的规则,调用约定是应用程序二进制接口 ABI 的一部分。

这些规则告诉我们:

  1. 调用者如何将参数传递给被调用者,因此,在入口点(即在函数的第一条指令执行之前)可以找到它们的位置。
  2. 被调用者如何将 return 值传递回调用者,以及调用者可以期望在哪里找到它们
  3. 被叫方如何知道哪个呼叫方return到
  4. 如果更改
  5. ,在 return 调用方之前必须将哪些寄存器恢复为原始值
  6. 我们可以依赖哪些寄存器为调用者进行调用而保留(这与 4 的寄存器集相同。)
  7. 哪些寄存器可以在被调用者中重新利用而无需 save/restore
  8. 哪个注册调用者不能依赖调用后保持不变(这与 6 相同。)

对于 MIPS:

参数在参数寄存器中传递,$a0$a1$a2$a3

Return 值在值寄存器中传递,$v0$v1.

return address(或更早的术语,链接)是一个指针参数,它告诉被调用者到哪里 return 以便转到正确的调用者。这是一个指向代码的指针,通常指的是调用者在调用后立即执行的指令。被调用者期望在 $ra 寄存器中找到这个值。你永远不会在 C 中看到 return 地址,但会在汇编中显示出来。

栈指针提供栈存储,MIPS上栈的规则是:

  • 在使用之前分配存储,
  • 解除分配与 return 调用方
  • 之前分配的一样多
  • 分配是通过从堆栈指针中减去来完成的
  • 从分配后堆栈指针指向的位置到之前的位置的内存是您的新内存。
  • 不言而喻,但不要写入您未分配的堆栈内存。

就函数体而言,如果你有函数的算法,那么函数体应该遵循那个算法。如果算法是用高级语言编写的,那么您应该知道每个结构化(控制)语句,如 if-then、if-then-else、while、do-while、for、是一个在汇编语言中具有等效模式的模式(表达式和嵌套语句)。表达式也一样:表达式可以分解成它们的部分并以机器代码执行。


Also how does the computer memory interact with the registers?

在机器代码级别,处理器提供物理存储。这包括 CPU 寄存器和主存储器。寄存器很快,直接在 CPU 里面,所以机器码指令直接对它们进行操作。主内存在 CPU 之外但很大。主内存是可寻址的,但 CPU 寄存器不是。任何需要引用或索引的数据结构都必须存储在主内存中——这很好,因为实际上没有足够的寄存器来存储一个非常小的数组。具有这些要求的数据结构包括:字符串、树、链表、数组,因此在主内存中分配。

说到这里,还要注意代码存储在主内存中,这意味着机器代码程序的每条机器代码指令都有一个唯一的内存地址。使用这个 属性,我们可以创建指向特定指令的指针,return 地址的概念使用了它。

机器代码程序(无论是由汇编程序员编写,还是由编译器翻译),将数据从主内存传输到寄存器并返回——在 MIPS 上,使用加载和存储指令。他们使用寄存器来完成计算,使用主存来存储数据结构。


此函数分配堆栈 space 并在其中存储参数,但不会进一步使用该内存,因此这是一种浪费,但无害。该函数确实在 returning 之前释放了已分配的 space,因为它是首先分配的(这里并不是真正需要的)。

sll开头的行做数组索引。他们使用 $s 寄存器违反了调用约定——允许使用这些寄存器,但前提是它们的传入值保留在 return 上,而在这里它们不是这样的这是违规行为。

索引单词(int)数组需要处理字节偏移量。字数组的第一个元素位于地址 A,例如,第二个字位于地址 A+4,因为第一个元素占用 4 个字节,每个字节也有一个地址。因此,索引单词数组的公式为 A+i*4,这就是该代码所做的。它索引两次(例如计算 A[i]、A[j] 的地址),从这些地址加载,然后将值存储回另一个来自的位置——这是一个交换操作。

这是C:

中的函数
void swap ( int *A, int i, int j ) {
    int *ai = &A[i];  // compute a pointer indexing A[i]
    int tempi = *ai;  // copy A[i]'s value from memory into local variable
    int *aj = &A[j];
    int tempj = *aj;
    *ai = tempj;      // store tempj (A[j]'s original value) back to A[i]
    *aj = tempi;      // and tempi (A[i]'s original value back to A[j]
}

我已经用公共子表达式消除法展示了它,其中 A[i] 和 A[j] 的地址计算用于读取和写入数组元素,就像在汇编中所做的那样。

根据调用,

A$a0 中,i$a1 中,j$a2 中调用者将遵循的约定。