最小汇编程序 ARM

Minimal assembly program ARM

我正在学习汇编,但在 arm 上使用 gnu/linux 时,我无法理解 CPU 是如何执行程序的。我会详细说明。

Problem:
    I want my program to return 5 as it's exit status.

此程序集是:

.text
.align 2
.global main
.type main, %function
main:
    mov w0, 5 //move 5 to register w0
    ret       //return

然后我 assemble 它与:

as prog.s -o prog.o

到目前为止一切正常。我知道我必须 link 我的目标文件到 C 库,以便添加额外的代码,使我的程序 运行。然后我 link with(为清楚起见省略了路径):

ld crti.o crtn.o crt1.o libc.so prog.o ld-linux-aarch64.so.1 -o prog

在此之后,一切都按预期工作:

./prog; echo $?
5

我的问题是我无法弄清楚 C 标准库在这里实际做了什么。我或多或少明白 crti/n/1 正在向我的程序添加入口代码(例如 .init 和 .start 部分),但不知道 libc 的目的是什么。

我对什么是“返回 5 作为退出状态”的最小汇编实现感兴趣

Web 上的大多数资源都集中在您进入 main 后的指令和程序流程上。我对使用 ./ 执行后执行的所有步骤非常感兴趣。我现在正在翻阅计算机体系结构的教科书,但我希望能在这里得到一点帮助。

C 语言从 main() 开始,但要使 C 语言正常工作,您通常至少需要最少的 bootstrap。例如,在可以从 C bootstrap

调用 main 之前
1) stack/stackpointer
2) .data initialized
3) .bss initalized
4) argc/argv prepared

然后是 C 库,其中有 many/countless 个 C 库,每个库都有自己的设计和要求,在调用 main 之前需要满足这些要求。 C 库对系统进行系统调用,因此这开始成为系统(操作系统,Linux、Windows 等)依赖性,具体取决于 C 库的设计,它可能是薄垫片或高度集成或介于两者之间。

同样,例如假设操作系统采用“二进制”(操作系统支持的二进制格式和该格式的规则由操作系统定义,并且工具链必须同样符合 C 库(即使您有时会看到相同的品牌名称,假设工具链和 C 库是独立的实体,一个设计用于与另一个一起工作))从非易失性媒体(如硬盘驱动器或 ssd)并将相关部分复制到内存中(流行的一部分,支持的二进制文件格式,用于调试或文件格式,而不是用于执行的实际代码或数据)。 因此,这留下了一个系统级设计选项,二进制文件格式是否指示 .data、.bss、.text 等(请注意,.data、.bss、.text 不是标准,只是约定,大多数人知道这意味着什么,即使特定工具链没有选择将这些名称用于部分甚至术语部分)。

如果是这样,获取程序并将其加载到内存中的操作系统加载程序可以选择将 .data 放在正确的位置并为您将 .bss 归零,这样 bootstrap 就不必这样做了。在 bare-metal 情况下,bootstrap 通常会处理 read/write 项目,因为它不是由其他软件从媒体加载的,它通常只是在处理器的地址 space 中在一些味道的 rom 上。

同样 argv/argc 可以由加载二进制文件的操作系统工具处理,因为假设操作系统 has/uses 是命令行,它必须从命令行解析出二进制文件的位置界面。但它可以简单地将命令行传递给 bootstrap 而 bootstrap 必须这样做,这些是系统级设计选择,与 C 无关,但与之前发生的事情有关main 被调用。

内存 space 规则由操作系统定义,在操作系统和 C 库之间定义,由于其私密性,通常包含 bootstrap 但我猜 C 库和 bootstrap 可以分开。所以 linking 也发挥了作用,这个操作系统是否支持保护,它只是 read/write 内存,你只需要在那里发送垃圾邮件,或者是否有单独的区域用于 read/only (.文本、.rodata 等)和 read/write(.data、.bss 等)。有人需要处理这个问题,linker 脚本和 bootstrap 通常有非常亲密的关系,linker 脚本解决方案特定于工具链,而不是假定为 portable ,为什么会这样,所以虽然还有其他解决方案,但常见的解决方案是有一个 C 库,其中包含 bootstrap 和 linker 解决方案,这些解决方案与操作系统和目标处理器密切相关。

然后你可以谈谈main()之后发生了什么。我很高兴看到您首先使用 ARM 而不是 x86 来学习,尽管 aarch64 对于第一个学习者来说是一场噩梦,不仅仅是指令集,只是执行级别和所有保护,您可以使用这种方法走很长一段路,但是如果不使用裸机,有些东西和一些说明是无法触及的。 (假设您使用的是 pi,那里有一个非常好的 bare-metal 论坛,其中有很多好的资源)。

gnu 工具使得 binutils 和 gcc 是独立但密切相关的项目。 gcc 知道事物相对于自身的位置,因此假设您将 gcc 与 binutils 和 glibc 结合使用(或者您只是使用找到的工具链),gcc 知道相对于它执行的位置来查找这些其他项目以及在调用 linker(gcc 在某种程度上只是一个 shell,它调用预处理器、编译器和汇编程序,然后 linker 如果没有指示不要做这些事情)。但是 gnu binutils linker 没有。虽然使用起来令人厌恶,但使用起来更容易

gcc test.o -o test

而不是在那天为那台机器弄清楚你在 ld 命令行上需要什么以及什么路径,并取决于参数在命令行上的设计顺序。

请注意,您至少可以避免这种情况

.global main
.type main, %function
main:
    mov w0, 5 //move 5 to register w0
    ret       //return

或查看 gcc 生成的内容

unsigned int fun ( void )
{
    return 5;
}

    .arch armv8-a
    .file   "so.c"
    .text
    .align  2
    .p2align 4,,11
    .global fun
    .type   fun, %function
fun:
    mov w0, 5
    ret
    .size   fun, .-fun
    .ident  "GCC: (GNU) 10.2.0"

我已经习惯在里面看到更多的绒毛了:

    .arch armv5t
    .fpu softvfp
    .eabi_attribute 20, 1
    .eabi_attribute 21, 1
    .eabi_attribute 23, 3
    .eabi_attribute 24, 1
    .eabi_attribute 25, 1
    .eabi_attribute 26, 2
    .eabi_attribute 30, 2
    .eabi_attribute 34, 0
    .eabi_attribute 18, 4
    .file   "so.c"
    .text
    .align  2
    .global fun
    .syntax unified
    .arm
    .type   fun, %function
fun:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    mov r0, #5
    bx  lr
    .size   fun, .-fun
    .ident  "GCC: (Ubuntu/Linaro 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
    .section    .note.GNU-stack,"",%progbits

无论哪种方式,您都可以查看每个汇编语言项目并决定您是否真的需要它们,部分取决于您是否觉得需要使用调试器或 binutils 工具来分解二进制文件(你真的需要知道吗例如为了学习汇编语言的乐趣大小?)

如果您想控制所有代码而不是 link 使用 C 库,我们非常欢迎您,您需要了解操作系统的内存 space 规则并创建一个linker 脚本(默认脚本可能部分与 C 库相关联,毫无疑问过于复杂,而不是您想要用作起点的脚本)。在这种情况下,在 main 中有两条指令,你只需要一个地址 space 对二进制文件有效,但是操作系统进入(理想情况下使用 ENTRY(标签),如果你愿意,它可以是 main 但通常不是 _start通常在 linker 脚本中找到,但也不是规则,您可以选择。正如评论中指出的那样,您需要进行系统调用才能退出程序。系统调用特定于操作系统,可能版本而不特定于目标 (ARM),因此您需要以正确的方式使用正确的版本,非常可行,您的整个项目 linker 脚本和汇编语言可能只有几十行代码总计。我们不是来这里 google 那些给你的,所以你会自己做。

你的部分问题是你正在寻找编译器解决方案,而编译器通常与这些完全无关。编译器将一种语言转换成另一种语言。汇编程序同样处理,但一个很简单,另一个通常是机器代码,位。 (一些编译器也输出位而不是文本)。这相当于查找 table 锯的用户手册来弄清楚如何建造房屋。 table 锯只是一种工具,您需要的工具之一,但只是一种通用工具。编译器,特定的 gnu 的 gcc,是通用的,它甚至不知道 main() 是什么。 Gnu沿袭了Unix的方式,所以它有一个单独的binutils和C库,单独开发,不想组合也可以不组合,可以单独使用。然后是操作系统,所以您的问题有一半隐藏在操作系统细节中,另一半隐藏在特定的 C 库或其他将 main() 连接到操作系统的解决方案中。

作为开源软件,您可以查看 bootstrap 的 glibc 和其他软件,看看它们做了什么。了解这类开源项目代码几乎不可读,有时反汇编容易得多,YMMV。

您可以搜索 arm aarch64 的 Linux 系统调用并找到用于退出的系统调用,您可能会看到您找到的开源 C 库或 bootstrap 解决方案被埋在下面你今天使用的是什么,将调用 exit 但如果没有,那么他们需要进行一些其他调用 return 返回操作系统。它不太可能是一个简单的 ret 和一个保存 return 值的寄存器,但从技术上讲,这是有人可以选择为他们的操作系统这样做的方式。

我想你会发现对于 Linux on arm,Linux 将解析命令行并在寄存器中传递 argc/argv,因此你可以简单地使用它们。并且可能会准备 .data 和 .bss,只要您正确构建二进制文件(您 link 它正确)。

这是一个最基本的例子。

运行 它与:

gcc -c thisfile.S && ld thisfile.o && ./a.out

源代码:

    #include <sys/syscall.h>
    .global _start
_start:
    movq $SYS_write, %rax
    movq ,         %rdi
    movq $st,        %rsi
    movq $(ed - st), %rdx
    syscall

    movq $SYS_exit,  %rax
    movq ,         %rdi
    syscall
st:
    .ascii "3[01;31mHello, OS World3[0m\n"
ed: