返回

AArch64汇编数组段错误排查及优化

Linux

AArch64 汇编数组操作导致段错误排查

问题根源分析

在 AArch64 汇编中操作数组时,段错误是比较常见的问题,根本原因常常归结于对内存的非法访问。具体到上面提供的代码,段错误发生在使用getchar读取字符后,向数组存储数据之时,这意味着数组访问方面可能存在一些隐患。
提供的代码示例中,分配了 256 字节的空间,但是使用过程中,我们无法保证用户输入的长度是可控的,所以要考虑几个关键点:数组边界、存储方式以及寄存器使用。

这段汇编代码,核心问题在 input_loop 的逻辑,看起来没有太多错误,但深入分析后就会发现以下潜在问题:

  1. check 函数调用: check函数没有必要,直接将cmp x4, #0;beq compute_checksum挪到主循环里,可以省去函数调用的开销和逻辑跳转的复杂性;并且,check函数存在潜在错误,每次 input_loop 结束前执行 bl check,而 check 内只是比较x4寄存器是否为 0 ,而无论什么结果都会进入 input_loop,循环是不会终止的;
  2. 数组溢出: 初始定义了一个256字节的数组空间。但是循环过程中只检查用户是否输入 EOF, 并没有数组的越界判断,所以存在用户输入字符数大于256导致缓冲区溢出的可能性。
  3. getchar 的返回值 : getchar 读取单个字符到 w0 寄存器,但其返回值也有可能不是有效字符,需考虑其出错时的边界值问题。
  4. 寄存器保存: 在调用函数 (printf,getchar) 时,可能会用到一些寄存器,这些寄存器如果未经保存就直接修改,可能会导致数据错误。虽然在这段代码中体现的不是很明显,但它是通用编程的注意事项。

解决方案

以下是一些针对性地解决段错误的方案。

1. 修正输入判断并增加边界检查

要避免缓冲区溢出,必须在读取每个字符后添加对数组边界的检查。可以简单地用一个变量 x7 表示数组的长度限制,并且,当输入的字符达到上限后停止循环。 getchar 返回 EOF 或者其它错误,我们也应该提前退出。

.data
array: .space 256
askC: .asciz "Enter a char value: "
fmt_check: .asciz "Checksum: %ld\n"

.text
.global main

main:
    mov x4, #10              // 设置最大读取字符数
    ldr x5, =array           //  x5 指向数组起始地址
    mov x6, #0              //  x6 用作校验和累加器
    mov x7, #256             //  x7 表示数组容量大小
input_loop:
    ldr x0, =askC
    bl printf              //  提示用户输入字符

    bl getchar               //  读取一个字符到 w0
    cmp w0, #0                 // 检查 EOF 或输入错误
    beq end                // 如果出错退出

    cmp x5, #array+256     // 比较当前指针和数组末尾
    beq compute_checksum     // 数组已满,计算校验和并结束
    strb w0, [x5]               // 将读取的字符存储在数组中

    uxtw x0, w0                  // 将 w0 中的字符转换为 x0,并零扩展
    add x6, x6, x0              // 计算校验和

    add x5, x5, #1             //  数组指针后移

    sub x4, x4, #1         // 将计数器减 1
    cmp x4,#0
    bne input_loop // 输入字符数量达到上限或者数组已经装满,跳转到校验和计算
compute_checksum:

    ldr x0, =fmt_check        // 加载格式化字符串地址
    mov x1, x6              // 设置校验和为 printf 的第二个参数
    bl printf                 //  打印校验和
end:
    mov x0, #0             // 返回状态码 0
    ret

此修改确保了读取数据不超过分配数组的大小,在input_loop中检查数组边界(cmp x5, #array+256;beq compute_checksum)。
以及检查用户输入是否错误 cmp w0, #0;beq end
每次循环,检查循环次数的限制(sub x4, x4, #1 ;cmp x4,#0) 。

2. 寄存器保存(虽然这段代码示例不太必要)

尽管提供的代码段在当前情况下不会因为寄存器冲突导致段错误,但良好的实践是在调用任何外部函数(例如printfgetchar)之前保存可能会被修改的寄存器。以下是如何保存和恢复寄存器(并非绝对必须,为代码规范):

// ... (先前代码)

input_loop:
    // 保存需要保护的寄存器,这里x4-x7和lr(返回地址)可能被使用到,视函数情况而定
    stp x4,x5,[sp,#-16]!
    stp x6,x7,[sp,#-16]!
    mov x9, lr
    stp x9, [sp, #-16]!
    ldr x0, =askC
    bl printf

    bl getchar
    // 恢复被调用方可能破坏的寄存器
    ldp x9, [sp], #16
    mov lr, x9
    ldp x6,x7,[sp],#16
    ldp x4,x5,[sp],#16

    cmp w0, #0
    beq end

//...(剩余代码,处理校验和)

end:
    mov x0, #0
    ret

我们使用 stp (store pair)和 ldp(load pair)来将寄存器推入堆栈和弹出,这是一种常见的方式,可以有效管理寄存器的保存与恢复,并且通过 mov x9, lr的方式保存链接寄存器lr的值。这段代码在本次问题的上下文环境中是多余的,但是建议了解这些概念,提高代码的稳定性。

操作步骤:

  1. 保存 代码到例如 checksum.s 的文件中。
  2. 汇编 文件:as checksum.s -o checksum.o
  3. 链接 文件:ld checksum.o -o checksum
  4. 执行./checksum
    用户将被提示输入字符,输入的字符达到数量或者缓冲区填满或者用户输入eof 时候计算校验和。

总结

解决 AArch64 汇编中的段错误,关键在于细致地检查内存操作和寄存器使用。数组越界、寄存器使用冲突都是潜在问题。编写代码时要认真地分析和解决这些问题,同时学习正确的寄存器使用规则。通过这种严谨的习惯,可以写出更健壮,安全的应用。

(本文并不提供外部资源,所有方法均可在ARM官方网站文档查到)。