Skip to content

LA32R汇编编程简介

汇编编程示例

Hello, world!

首先我们先实现一个最经典的例子,即向屏幕中输出"Hello, world!"。在这个例子中,我们可以了解汇编程序的基本结构,掌握函数作为调用者和被调用者需要考虑的寄存器使用规范。

来看看它的c语言描述:

hello.c
1
2
3
4
int main() {
    puts("Hello, world!\n");
    return 0;
}

尝试对这个c语言描述进行翻译。

字符串"Hello, world!"需要存储于数据段.data,给其一个可以访问的标号,记为output

hello.S
.section .data
    output: .asciz "Hello, world!\n"

代码部分位于.text代码段。程序的入口地址main必须设置为全局符号。有读者在写其他语言汇编时可能将程序入口地址设置为_start,实际上,系统有_start函数。而系统的_start会首先做初始化工作,之后跳转至main标号处,结束后会进行回收工作。如果我们将用户程序入口地址定义为_start标号,则会将系统的函数覆盖掉。有的链接器允许进行这样的操作。而la32r的链接器则会报告对_start的多重定义错误(multiple definition of`_start'),以及未找到main错误(undefined reference to`main')。因此我们在定义程序入口时,应当定义一个全局的main标号。

hello.S
3
4
5
.section .text
.globl main
main:

考虑到需要调用puts函数,$ra可能被毁,因此需要保存到栈帧中。如果该函数就是叶子函数,则不需要保存$ra。相应地,栈指针只需要减小一个寄存器长度,即4字节。(第6、7行)

在返回到main的调用函数前,需要恢复$ra$sp。(第11、12行)

函数调用时,针对参数需要考虑两个问题:参数存在哪?存什么内容?puts只有一个参数,根据寄存器使用约定,$a0是第一个参数寄存器,因此存入$a0中。那么$a0中存什么呢?根据

int puts(const char *str);

c函数的声明,可以看到它的参数是地址,因此将需要输出的字符串地址通过la.local指令加载到$a0中。(第8、9行)读者可以思考一下,对于printf("%d\n", a);scanf("%d", &a);参数应该分别是什么形式。

在返回到main的调用函数前,还需要设置main函数的返回值为0,存入返回值寄存器$v0(即$a0)中。(第10、13行)

hello.S
    addi.w      $sp, $sp, -4
    st.w        $ra, $sp, 0
    la.local    $a0, output
    bl          %plt(puts)
    li.w        $a0, 0
    ld.w        $ra, $sp, 0
    addi.w      $sp, $sp, 4
    jr          $ra


Hello, world! syscall版本

此时我们聚焦于函数的主体功能部分,通过这个例子中,我们可以简单了解系统调用的汇编程序编写方法。

puts在更底层是通过sys_write来实现的。我们可以尝试使用sys_write实现打印功能。首先考察sys_write的参数格式,其他syscall函数我们可以查阅linux源码include/linux/syscalls.h中定义的函数:

long sys_write(unsigned int fd, const char __user *buf, size_t count);

第一个参数fd文件描述符,在POSIX语义中,0、1、2这三个值已经被赋予特殊含义,分别是标准输入(STDIN_FILENO),标准输出(STDOUT_FILENO),标准错误(STDERR_FILENO)。其他文件描述符用于描述打开的文件。在本示例中,因为是向屏幕输出,我们设置fd = 1

第二个参数即为前述output

第三个参数为字符串长度,这个需要手动计数一下。

参数都确定好之后,我们需要回忆一下系统调用的约定。和函数调用不同,我们需要将系统调用号存放于$a7中,用剩余的$a0 ~ $a6寄存器传递参数。在内核文件include/uapi/asm-generic/unistd.h中查询系统调用号,可知sys_write的系统调用号为64。因此能够写出以下代码,同样达到向屏幕打印的效果。

hello_sys.S
    li.w        $a7, 64     # syscall number
    li.w        $a0, 1      # fd
    la.local    $a1, output # buf
    li.w        $a2, 14     # count
    syscall     0


bubble sort

这个示例希望能够增强读者对函数调用过程,寄存器使用规范,以及if/for/双重for控制流的理解。这里先给出它的c语言版本,读者可以尝试写出这段程序的汇编语言版本,并在我们的实验平台上运行以及调试,如遇困难再进行后面的阅读。

bubble_sort.c
int arr[50];
int len;

void bubble_sort(int* array, int len) {
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len  1  i; j++) {
            if (array[j] > array[j + 1]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
        }
    }
}

int main() {
    scanf("%d", &len);
    for (int i = 0; i < len; i++) arr[i] = rand() % 100;

    for (int i = 0; i < len; i++) printf("%d ", arr[i]);
    printf("\n");

    bubble_sort(arr, len);

    for (int i = 0; i < len; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

这个例子的main函数相对比较复杂。观察到main函数主要完成五部分内容:scanfrandprintfbubble_sortprintf。我们逐个击破。

scanf:从标准输入中读取len

函数调用前解决参数问题:

int scanf(const char *format, ...);

scanf("%d", &a);

第一个参数是字符串地址,之后的参数是变量的地址。因此在本例中,我们需要提前定义好字符串"%d"以及len变量,以便于后续得到地址。

bubble_sort.S: .data
1
2
3
.section .data
len: .int 0
input_format: .asciz "%d"
bubble_sort.S: scanf
1
2
3
    la.local    $a0, input_format
    la.local    $a1, len
    bl          scanf

rand:根据len值生成len个随机数,存入arr

单重for循环控制流 我们可以使用分支跳转类指令一节提到的for循环结构,这里再介绍一种结构,可以在循环主体减去一条分支跳转类指令,如果已知循环至少执行一次,那么第3行还能再省去:

1
2
3
4
5
6
7
8
    la.local    $t0, len
    ld.w        $t0, $t0, 0
    bne         $t0, $zero, loop
    jr          $ra
loop:
    <do_something>
    addi.w      $t0, $t0, -1
    bgt         $t0, $zero, loop

然而在实际循环中,仍有两方面需要注意:

  1. 如果<do_something>中有函数的调用,那么存放循环计数的寄存器就不能直接用被调用者不保存的寄存器,如$t##$a##。那么解决方法,可以是将$t##<do_something>前保存至栈中,在<do_something>后取出使用,该方法会在循环中产生较多访存;也可以考虑使用$s##作为循环计数变量,而代价是使用前需要将旧值存入栈中,以及在该函数返回前将其恢复出来。直观感觉是第二个方法更有利于循环的高效执行。

  2. 虽然我们在c语言中常使用的循环增量为1,但如果考虑到这个循环计数值我们将用于数组的访问,我们可以根据数据宽度设置循环增量,并且循环初始值、结束条件可以稍作调整,将其进行等价转换:

    1
    2
    3
    4
    5
    6
    7
        // before
        for (int i = 0; i < len; i++) arr[i] = rand() % 100;
    
        // after    
        uint32_t addr;
        for (addr = &arr[0]; addr <= &arr[len - 1]; addr += 4) 
            *(int*)addr = rand() % 100;
    

    如果我们用一个寄存器作为循环计数寄存器,那么在获取arr[i]时,需要完成addr = (uint32_t)&arr[0] + i * 4的计算。我们可以转换成汇编做一个对比。

    # before
        # $t0 = i
        # $t1 = &arr[0]
        # $t3 = store value
    loop:
        slli.w      $t2, $t0, 2     # $t2 = i * 4
        add.w       $t2, $t1, $t2   # $t2 = i * 4 + &arr[0]
        st.w        $t3, $t2, 0
        addi.w      $t0, $t0, 1
    
    # after
        # $t0 = &arr[i]
        # $t1 = store value
    loop:
        st.w        $t1, $t0, 0
        addi.w      $t0, $t0, 4
    

数组定义len类似,数组需要提前在数据段定义才能进行访问。这里我们可以采用之前提到的.rept ... .endr汇编指示来定义50个变量。

函数返回 这里rand是一个无参数但是有返回值的函数,注意返回值存于$a0中。

那么可以开始编写这部分内容。

bubble_sort.S: .data
1
2
3
4
.section .data
value_array: .rept 50
             .int 0
             .endr
bubble_sort.S: main: rand
.section .text
.globl main
main:
    addi.w      $sp, $sp, -16
    st.w        $ra, $sp, 0
    st.w        $s0, $sp, 4         # s0 = &value_array
    st.w        $s1, $sp, 8         # s1 = len
    st.w        $s2, $sp, 12

    ...

    la.local    $s0, value_array    # $s0 = &arr[0]
    la.local    $s1, len
    ld.w        $s1, $s1, 0         # $s1 = len

    move        $s2, $s1
    addi.w      $s2, $s2, -1
    slli.w      $s2, $s2, 2
    add.w       $s2, $s0, $s2       # $s2 = &arr[len - 1]

loop_r:
    bl          rand
    li.w        $t0, 100            # 使用$t0寄存器,每次循环都需要加载
    mod.w       $t0, $a0, $t0
    st.w        $t0, $s2, 0
    addi.w      $s2, $s2, -4        # 倒着生成的随机值
    bge         $s2, $s0, loop_r

    ...

    ld.w        $ra, $sp, 0
    ld.w        $s0, $sp, 4
    ld.w        $s1, $sp, 8
    ld.w        $s2, $sp, 12
    addi.w      $sp, $sp, 16
    jr          $ra

printf:打印数组值

注意到main中两次printf的操作内容是一样的,我们可以将其提取为一个公共函数:

1
2
3
4
void output_data(int* array, int len) {
    for (int i = 0; i < len; i++) printf("%d ", arr[i]);
    puts("\n");
}

for循环的写法与注意事项已在前一小节提及,这里再强调一下printf的写法。

int printf(const char *format, ...);

printf("%d ", a);

可以看到printf包含两个参数,字符串"%d "和整形变量a,字符串变量需在数据段定义。整形变量即为数组第i项的值(注意不是地址),此处涉及访存。

bubble_sort.S: .data
1
2
3
.section .data
print_format: .asciz "%d "
print_line: .asciz "\n"
bubble_sort.S: main: output_data
1
2
3
    move        $a0, $s0            # arg1: start_addr = &value_array
    move        $a1, $s1            # arg2: len
    bl          output_data
bubble_sort.S: printf
.type output_data, @function
output_data:
    addi.w      $sp, $sp, -16
    st.w        $ra, $sp, 0
    st.w        $s0, $sp, 4
    st.w        $s1, $sp, 8
    st.w        $s2, $sp, 12

    move        $s0, $a1            # s0 = len
    move        $s1, $a0            # s1 = value addr
    la.local    $s2, print_format   # s2 = printf arg1(print_format)
loop_o:
    move        $a0, $s2            # a0 = s2
    ld.w        $a1, $s1, 0         # a1 = printf arg2(value)
    bl          printf
    addi.w      $s1, $s1, 4
    addi.w      $s0, $s0, -1
    bne         $s0, $zero, loop_o

    la.local    $a0, print_line
    bl          puts

    ld.w        $ra, $sp, 0
    ld.w        $s0, $sp, 4
    ld.w        $s1, $sp, 8
    ld.w        $s2, $sp, 12
    addi.w      $sp, $sp, 16
    jr          $ra

bubble_sort:冒泡排序

这是一个双重for循环的控制流,在编写for循环时,我们需要想清楚初始值是什么,终止条件是什么,循环增量怎么设置,循环主体要做什么。

前面我们提到,出于访问数组的便捷性考虑,我们会将循环计数变量改造为每次访问的地址,这里进行相同的改造:

    // before
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len  1  i; j++) {
            if (array[j] > array[j + 1]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
        }
    }

    // after
    uint32_t addr_i, addr_j;
    for (addr_i = &array[0]; addr_i < &array[len - 1]; addr_i += 4) {
        for (addr_j = (uint32_t)&array[0]; \
             addr_j <= (uint32_t)&array[len - 1] - 4 * i; \
             addr_j += 4) {
            int a = *(int*)addr_j;
            int b = *(int*)(addr_j + 4);
            if (a > b) {
                *(int*)addr_j = b;
                *(int*)(addr_j + 4) = a;
            }
        }
    }

注意到这里循环内部没有函数调用,所以我们可以放心大胆地使用$t##$a##

分析一下寄存器分配方案:

  1. addr_j每次都从&array[0]开始,所以必须存&array[0]$a0作为参数天然存了这个值。

  2. addr_i要与终止条件&array[len - 1]判断,因此要存&array[len - 1]$a1原本存len,经过(len - 1) << 2 + $a0的变换可以得到,直接存于$a1中即可。

  3. 存储addr_i需要一个寄存器,记为$a2;存储addr_j需要一个寄存器,记为$t0

  4. addr_j的终止值不用每次都计算,可以在每次外层循环更新变量时更新内层循环终止值,初始设为$a1,每次减4即可,记为$t1

  5. 需要获取addr_jaddr_j + 4地址处的值,分别记为$t2$t3

据此,可以设计bubble_sort的代码了。

完整汇编程序

这里给读者提供一个能够正确实现bubble sort的汇编代码示例,仅供参考。

bubble_sort.S
.section .data
value_array: .rept 50
             .int 0
             .endr
len: .int 0
input_format: .asciz "%d"
print_format: .asciz "%d "
print_line: .asciz "\n"

.section .text
.globl main
main:
    addi.w      $sp, $sp, -16
    st.w        $ra, $sp, 0
    st.w        $s0, $sp, 4         # s0 = &value_array
    st.w        $s1, $sp, 8         # s1 = len
    st.w        $s2, $sp, 12

    la.local    $a0, input_format
    la.local    $a1, len
    bl          scanf

    la.local    $s0, value_array
    la.local    $s1, len
    ld.w        $s1, $s1, 0

    move        $s2, $s1
    addi.w      $s2, $s2, -1
    slli.w      $s2, $s2, 2
    add.w       $s2, $s0, $s2

loop_r:
    bl          rand
    li.w        $t0, 100
    mod.w       $t0, $a0, $t0
    st.w        $t0, $s2, 0
    addi.w      $s2, $s2, -4
    bge         $s2, $s0, loop_r

    move        $a0, $s0            # arg1: start_addr = &value_array
    move        $a1, $s1            # arg2: len
    bl          output_data

    move        $a0, $s0            # arg1: start_addr = &value_array
    move        $a1, $s1            # arg2: len
    bl          bubble_sort

    move        $a0, $s0            # arg1: start_addr = &value_array
    move        $a1, $s1            # arg2: len
    bl          output_data

    move        $a0, $zero
    ld.w        $ra, $sp, 0
    ld.w        $s0, $sp, 4
    ld.w        $s1, $sp, 8
    ld.w        $s2, $sp, 12
    addi.w      $sp, $sp, 16
    jr          $ra

.type bubble_sort, @function
bubble_sort:
    addi.w      $t0, $zero, 1
    bne         $a1, $t0, sort
    jr          $ra
sort:
    addi.w      $sp, $sp, -4
    st.w        $ra, $sp, 0
                                    # a0 = value start addr
    move        $a2, $a0            # a2 = i start addr
    addi.w      $a1, $a1, -1
    slli.w      $a1, $a1, 2
    add.w       $a1, $a1, $a0       # a1 = value end addr
    move        $t1, $a1            # t1 = j end addr, i loop changes

loop_i:
    move        $t0, $a0            # t0 = j start addr, j loop changes

loop_j:
    ld.w        $t2, $t0, 0         # t2 = value[j]
    ld.w        $t3, $t0, 4         # t3 = value[j+1]
    ble         $t2, $t3, noswap
    st.w        $t2, $t0, 4
    st.w        $t3, $t0, 0

noswap:
    addi.w      $t0, $t0, 4
    bne         $t0, $t1, loop_j

    addi.w      $a2, $a2, 4
    addi.w      $t1, $t1, -4
    bne         $a2, $a1, loop_i

end_loop_i:
    ld.w        $ra, $sp, 0
    addi.w      $sp, $sp, 4
    jr          $ra

.type output_data, @function
output_data:
    addi.w      $sp, $sp, -16
    st.w        $ra, $sp, 0
    st.w        $s0, $sp, 4
    st.w        $s1, $sp, 8
    st.w        $s2, $sp, 12

    move        $s0, $a1            # s0 = len
    move        $s1, $a0            # s1 = value addr
    la.local    $s2, print_format   # s2 = printf arg1(print_format)
loop_o:
    move        $a0, $s2            # a0 = s2
    ld.w        $a1, $s1, 0         # a1 = printf arg2(value)
    bl          printf
    addi.w      $s1, $s1, 4
    addi.w      $s0, $s0, -1
    bne         $s0, $zero, loop_o

    la.local    $a0, print_line
    bl          puts

    ld.w        $ra, $sp, 0
    ld.w        $s0, $sp, 4
    ld.w        $s1, $sp, 8
    ld.w        $s2, $sp, 12
    addi.w      $sp, $sp, 16
    jr          $ra