最小装配程序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

然后我把它组装成:

as prog.s -o prog.o

到这里一切都好。我知道我必须将我的对象文件链接到C库,以便添加额外的代码来运行我的程序。然后我链接到(为了清晰起见,省略了路径):

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作为退出状态";

网络上的大多数资源都集中在指令和程序流上。我真的很感兴趣的是,一旦我用它执行,所有的步骤都会发生什么。/。我现在正在复习计算机体系结构的课本,但我希望我能在这里得到一些帮助。

C语言从main()开始,但要使C正常工作,您通常至少需要一个最小的引导程序。例如,可以从C引导调用main之前

1) stack/stackpointer
2) .data initialized
3) .bss initalized
4) argc/argv prepared

然后是C库,有很多/无数的C库,在调用main之前,每个库都有自己的设计和需求需要满足。C库对系统进行系统调用,因此这开始变得依赖于系统(操作系统、Linux、Windows等),这取决于C库的设计,它可能是一个薄垫片,也可能是高度集成的,或者介于两者之间。

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

如果是这样的话,获取程序并将其加载到内存中的操作系统加载程序可以选择将.data放在正确的位置,并为您设置零.bss,这样引导程序就不必这样做了。在裸机情况下,引导程序通常会处理读/写项,因为它不是由其他软件从介质加载的,它通常只是在rom上处理器的地址空间中有些味道。

同样,argv/argc可以由加载二进制文件的操作系统工具处理,因为它必须从命令行解析出二进制文件的位置,假设操作系统具有/使用命令行接口。但它可以很容易地将命令行传递给引导程序,而引导程序必须这样做,这些都是系统级的设计选择,与C无关,而是与调用main之前发生的事情有关。

内存空间规则由操作系统定义,并在操作系统和C库之间定义,由于其密切的性质,C库通常包含引导程序,但我想C库和引导程序可能是分开的。所以链接也起到了一定的作用,这个操作系统支持保护吗?它只是读/写内存,你只需要在那里发送垃圾邮件,或者有单独的只读(.text、.rodata等)和读/写(.data、.bss等)区域。有人需要处理这一问题,链接器脚本和引导程序通常有着非常密切的关系,而链接器脚本解决方案是特定于一个不被认为是可移植的工具链的,为什么会这样呢?因此,尽管有其他解决方案,但常见的解决方案是有一个C库,其中包含与操作系统和目标处理器紧密相关的引导程序和链接器解决方案。

然后你可以讨论main()之后会发生什么。我很高兴看到你首先使用ARM而不是x86来学习,尽管aarch64对第一个来说是一场噩梦,而不是指令集,只是执行级别和所有的保护,你可以用这种方法走很长的路,但有些东西和指令是你无法触及的。(假设你使用的是pi,有一个非常好的裸金属论坛,有很多好的资源)。

gnu工具使得binutils和gcc是独立但密切相关的项目。gcc知道事物相对于自身的位置,所以假设你将gcc与binutils和glibc结合在一起(或者你只使用你找到的工具链),gcc就知道它在哪里执行以找到这些其他项,以及当它调用链接器时要传递什么项(在某种程度上,gcc只是一个shell,它调用预处理器、编译器、汇编程序,如果没有指示不要做这些事情,则调用链接器)。但是gnu-binutils链接器没有。虽然使用起来很恶心,但更容易

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工具来拆分二进制文件(例如,为了学习汇编语言,你真的需要知道乐趣的大小吗?)

如果您希望控制所有代码而不与C库链接,我们非常欢迎您这样做。您需要了解操作系统的内存空间规则并创建链接器脚本(默认脚本可能部分与C库绑定,毫无疑问过于复杂,不想用作起点)。在这种情况下,如果是两条主指令,你只需要一个对二进制有效的地址空间,但操作系统会输入(理想情况下使用ENTRY(标签),如果你想的话,它可以是主指令,但通常不是。start通常在链接器脚本中找到,但也不是规则,你可以选择。正如评论中所指出的,您需要进行系统调用才能退出程序。系统调用是特定于操作系统的,可能是特定于版本的,而不是特定于目标(ARM)的,所以你需要以正确的方式使用正确的调用,非常可行,你的整个项目链接器脚本和汇编语言可能总共有几十行代码。我们不是来为你搜索这些的,所以你可以自己搜索。

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

作为开源,你可以去看看glibc和其他人的引导程序,看看他们做了什么。了解这种类型的开源项目,代码几乎是不可读的,有时更容易反汇编,YMMV。

您可以搜索arm aarch64的Linux系统调用,并找到一个用于退出的调用,您可能会看到,您发现的隐藏在当前使用的内容下的开源C库或引导程序解决方案将调用退出,但如果没有,则需要进行其他调用才能返回操作系统。这不太可能是一个简单的ret,它有一个保存返回值的寄存器,但从技术上讲,这就是人们可以为他们的操作系统选择这样做的方式。

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

这里是一个最简单的例子。

使用运行

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

源代码:

#include <sys/syscall.h>
.global _start
_start:
movq $SYS_write, %rax
movq $1,         %rdi
movq $st,        %rsi
movq $(ed - st), %rdx
syscall
movq $SYS_exit,  %rax
movq $1,         %rdi
syscall
st:
.ascii "33[01;31mHello, OS World33[0mn"
ed: 

最新更新