ARM64 架构基础知识

实验:汇编语言练习——查找最大数

本实验我们用汇编语言写一个查找最大数功能,借此了解和熟悉 ARM64 汇编语言。

代码清单 1 查找最大数
  1. .section .data
  2. .align 3
  3. my_data:
  4.     .quad   1
  5.     .quad   2
  6.     .quad   5
  7.     .quad   8
  8.     .quad   10
  9.     .quad   12
  10.  
  11. my_data_count:
  12.     .quad   6
  13.  
  14. .align 3
  15. print_data:
  16.     .string "max data: %d\n"
  17.  
  18. .section .text
  19. .globl main
  20. main:
  21.     stp x29, x30, [sp, -16]!
  22.     ldr x0, =my_data
  23.     ldr x1, my_data_count
  24.     add x4, x0, #40
  25.  
  26.     mov x3, xzr
  27. 1:
  28.     ldr x2, [x0], #8
  29.     cmp x2, x3
  30.     csel x3, x2, x3, hi
  31.  
  32.     cmp x0, x4
  33.     b.ls 1b
  34.  
  35.     ldr x0, =print_data
  36.     mov x1, x3
  37.     bl printf
  38.  
  39.     ldp x29, x30, [sp], 16
  40.     ret

功能方面没什么进一步详细说的地方了,功能就是查找定义好的数值里的最大数,并打印出来。我们就从汇编指令着手,一个一个了解。

stp : Store Pair,用于将两个通用寄存器中的值存储到内存中。语法为 stp <Rt1>, <Rt2>, [<Xn|SP>, #-<imm>]!,最后的感叹号表示在存储数据之后,自动更新基地址的值,以便下一次存储操作使用更新后的地址。stp x29, x30, [sp, -16]! 的意思是将 x29 和 x30 寄存器中的值存储到 SP 寄存器减去 16 的地址处,并将 SP 寄存器的值更新为存储后的地址。

x29 是帧指针寄存器(Frame Pointer,FP)。FP 寄存器用于保存函数调用者的栈帧指针。在函数调用时,将调用者的 FP 值保存到栈中,并将自己的 FP 设置为栈顶指针(即 SP 寄存器的值)。这样,函数可以通过 FP 寄存器访问调用者的栈帧,并通过相对地址访问函数参数和局部变量。

x30 是链接寄存器(Link Register,LR)。LR 寄存器用于保存函数调用的返回地址。在函数调用时,将返回地址存储在 LR 寄存器中,并将控制转移至被调用函数。在函数返回时,将 LR 寄存器中的值恢复到程序计数器(PC)中,以便返回到调用者的代码位置。

xzr : 零寄存器(Zero Register)。零寄存器中的值恒为 0,且是只读的。

csel : 条件选择(Conditional Select)。语法为 csel Rd, Rn, Rm, cond。其中 cond 是条件代码,如果条件码为真,则将 Rn 的值存储到 Rd 中;否则将 Rm 的值存储到Rd中。

b.ls : b 是无条件分支指令(branch)。ls 是条件代码,代表小于或等于(lower or same)。

接着我们用 gcc 进行交叉编译(因为是 arm 指令):

  • tim@tim:~$ aarch64-linux-gnu-gcc max.S -o max --static

运行结果符合预期:

  • tim@tim:~$ qemu-aarch64-static max
  • max data: 12

没有额外的链接操作,直接就可以调用到标准库的 printf。原因是 gcc 默认就会链接 libc 库。

实验:通过 C 语言调用汇编函数

如代码清单 2.1 所示,我们先实现汇编版本的返回最大数函数。

代码清单 2.1 汇编函数
  1. .section .text
  2. .globl my_max
  3. my_max:
  4.     cmp x0, x1
  5.     csel x0, x0, x1, hi
  6.     ret

如代码清单 2.2 所示,在 C 程序里调用定义的汇编函数。

代码清单 2.2 调用汇编函数
  1. #include <stdio.h>
  2.  
  3. int my_max(int a, int b);
  4.  
  5. int main()
  6. {
  7.     int max = my_max(5, 6);
  8.     printf("max data: %d\n", max);
  9.     return 0;
  10. }

编译:

  • tim@tim:~$ aarch64-linux-gnu-gcc max.S main.c -o main --static

运行符合预期:

  • tim@tim:~$ qemu-aarch64-static main
  • max data: 6

实验:通过汇编语言调用 C 函数

如代码清单 3.1 所示,我们先实现 C 语言版本的返回最大值函数。

代码清单 3.1 C 函数
  1. int my_max(int a, int b)
  2. {
  3.     return (a >= b) ? a : b;
  4. }

如代码清单 3.1 所示,我们调用定义的 C 语言函数,整体框架和代码清单 1 是一样的。

代码清单 3.1 调用 C 函数
  1. .section .data
  2. .align 3
  3.  
  4. print_data:
  5.     .string "max data: %d\n"
  6.  
  7. .section .text
  8. .global main
  9. main:
  10.     stp x29, x30, [sp, -16]!
  11.  
  12.     mov x0, #6
  13.     mov x1, #5
  14.     bl my_max
  15.  
  16.     mov x1, x0
  17.     ldr x0, =print_data
  18.     bl printf
  19.  
  20.     ldp x29, x30, [sp], 16
  21.     ret
  • tim@tim:~$ aarch64-linux-gnu-gcc max.c main.S -o main --static

运行符合预期:

  • tim@tim:~$ qemu-aarch64-static main
  • max data: 6

实验:GCC 内联汇编

我们先看代码清单 4,是 GCC 内联汇编实现的返回最大数函数。

代码清单 4 内联汇编
  1. #include <stdio.h>
  2.  
  3. int my_max(int a, int b)
  4. {
  5.     int val;
  6.  
  7.     asm volatile (
  8.         "cmp %1, %2\n"
  9.         "csel %0, %1, %2, hi\n"
  10.         : "+r" (val)
  11.         : "r" (a), "r" (b)
  12.         : "memory"
  13.         );
  14.  
  15.     return val;
  16. }
  17.  
  18. int main()
  19. {
  20.     int max = my_max(5, 6);
  21.     printf("max data: %d\n", max);
  22.     return 0;
  23. }

GCC 内联汇编的语法格式为:asm("assembly code" : output : input : clobber);。其中 "assembly code" 是嵌入的汇编代码;output 是输出的寄存器或变量;input 是输入的寄存器和变量;clobber 是需要维护的破坏上下文的寄存器。

“输入”是指 C 代码传递给汇编代码的数据。

“输出”是值汇编代码传递给 C 代码的数据。

内联汇编中的 %0 等符号对应着约束中指定的操作数,即 output 为开始的索引。