我在四处游玩,并试图了解计算机和程序的低级操作。为此,我正在尝试将Assembly和C链接起来。
我有2个程序文件:
“ callee.c”中的某些C代码:
#include <unistd.h>
void my_c_func() {
write(1, "Hello, World!\n", 14);
return;
}
我在“ caller.asm”中也有一些GAS x86_64组件:
.text
.globl my_entry_pt
my_entry_pt:
# call my c function
call my_c_func # this function has no parameters and no return data
# make the 'exit' system call
mov $60, %rax # set the syscall to the index of 'exit' (60)
mov $0, %rdi # set the single parameter, the exit code to 0 for normal exit
syscall
我可以这样构建并执行程序:
$ as ./caller.asm -o ./caller.obj
$ gcc -c ./callee.c -o ./callee.obj
$ ld -e my_entry_pt -lc ./callee.obj ./caller.obj -o ./prog.out -dynamic-linker /lib64/ld-linux-x86-64.so.2
$ ldd ./prog.out
linux-vdso.so.1 (0x00007fffdb8fe000)
libc.so.6 => /lib64/libc.so.6 (0x00007f46c7756000)
/lib64/ld-linux-x86-64.so.2 (0x00007f46c7942000)
$ ./prog.out
Hello, World!
一路走来,我遇到了一些问题。如果我未设置-dynamic-linker选项,则默认为:
$ ld -e my_entry_pt -lc ./callee.obj ./caller.obj -o ./prog.out
$ ldd ./prog.out
linux-vdso.so.1 (0x00007ffc771c5000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8f2abe2000)
/lib/ld64.so.1 => /lib64/ld-linux-x86-64.so.2 (0x00007f8f2adce000)
$ ./prog.out
bash: ./prog.out: No such file or directory
为什么?我的系统上的链接器默认值是否存在问题?我如何/应该解决它?
此外,静态链接也不起作用。
$ ld -static -e my_entry_pt -lc ./callee.obj ./caller.obj -o ./prog.out
ld: ./callee.obj: in function `my_c_func':
callee.c:(.text+0x16): undefined reference to `write'
为什么?不应该write()只是syscall'write'的C库包装器吗?我该如何解决?
我在哪里可以找到有关C函数调用约定的文档,因此我可以阅读如何来回传递参数,等等...
最后,虽然这对于这个简单的示例似乎可行,但是我在初始化C堆栈时是否做错了什么?我的意思是,现在,我什么也没做。在开始尝试调用函数之前,是否应该从内核为堆栈分配内存,设置边界以及设置%rsp和%rbp。还是内核加载程序为我完成所有这些工作?如果是这样,Linux内核下的所有体系结构都会为我照顾吗?
虽然Linux内核提供了一个名为write
的系统调用,但这并不意味着您会自动获得同名的函数。实际上,您需要内联汇编才能从C调用任何系统调用。
而不是将二进制文件与ld
明确链接,而是让gcc
为您完成。如果源以as
后缀结尾,它甚至可以编译程序集文件(内部执行.s
的合适版本)。看起来您的链接问题只是GCC承担的假设与您自己通过LD进行操作之间的分歧。不,这不是错误。
Linux使用System V ABI。 AMD64 Architecture Processor Supplement(PDF)描述了初始执行环境(当调用_start
时)和调用约定。本质上,您已经初始化了堆栈,并在其中存储了环境和命令行参数。
让我们构造一个完整的示例,其中包含C和汇编(AT&T语法)源,以及最终的静态和动态二进制文件。
首先,我们需要Makefile
来保存键入的长命令:
# SPDX-License-Identifier: CC0-1.0
CC := gcc
CFLAGS := -Wall -Wextra -O2 -fPIC -pie -march=x86-64 -mtune=generic -m64 \
-ffreestanding -nostdlib -nostartfiles
LDFLAGS :=
all: static-prog dynamic-prog
clean:
rm -f static-prog dynamic-prog *.o
%.o: %.c
$(CC) $(CFLAGS) $^ -c -o $@
%.o: %.s
$(CC) $(CFLAGS) $^ -c -o $@
dynamic-prog: main.o asm.o
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
static-prog: main.o asm.o
$(CC) -static $(CFLAGS) $^ $(LDFLAGS) -o $@
Makefile的缩进特别突出,但是此论坛将制表符转换为空格。因此,粘贴以上内容后,运行sed -e 's|^ *|\t|' -i Makefile
将压痕修复回制表符。
上面的Makefile和所有随后的文件中的SPDX许可证标识符告诉您这些文件是根据Creative Commons Zero license许可的:也就是说,这些文件都专用于公共领域。
使用的编译标志:
-Wall -Wextra
:启用所有警告。这是一个好习惯。
-O2
:优化代码。这是常用的优化级别,通常认为足够,但又不过分。
-fPIC -pie
:编译位置无关代码,并创建位置无关可执行文件。如果您不知道或不在乎,则可以忽略这些。
-march=x86-64 -mtune=generic -m64
:编译为64位x86-64 AKA AMD64架构。
-ffreestanding
:编译针对freestanding C环境。
-nostdlib -nostartfiles
:请勿在标准C库或其启动文件中链接。
接下来,我们创建一个头文件nolib.h
,该文件在group_exit周围实现nolib_exit()
和nolib_write()
包装并编写syscalls:
// SPDX-License-Identifier: CC0-1.0
/* Require Linux on x86-64 */
#if !defined(__linux__) || !defined(__x86_64__)
#error "This only works on Linux on x86-64."
#endif
/* Known syscall numbers */
#define SYS_write 1
#define SYS_exit_group 231
/* Inline assembly macro for a single-parameter no-return syscall */
#define SYSCALL1_NORET(nr, arg1) \
__asm__ volatile ( "syscall\n\t" : : "a" (nr), "D" (arg1) : "rcx", "r11" )
/* Inline assembly macro for a three-parameter syscall */
#define SYSCALL3(retval, nr, arg1, arg2, arg3) \
__asm__ volatile ( "syscall\n\t" : "=a" (retval) : "a" (nr), "D" (arg1), "S" (arg2), "d" (arg3) : "rcx", "r11" )
/* exit() function */
static inline void nolib_exit(int retval)
{
SYSCALL1_NORET(SYS_exit_group, retval);
}
/* Some errno values */
#define EINTR 4 /* Interrupted system call */
#define EBADF 9 /* Bad file descriptor */
#define EINVAL 22 /* Invalid argument */
/* write() syscall wrapper - returns negative errno if an error occurs */
static inline long nolib_write(int fd, const void *data, long len)
{
long retval;
if (fd == -1)
return -EBADF;
if (!data || len < 0)
return -EINVAL;
SYSCALL3(retval, SYS_write, fd, data, len);
return retval;
}
nolib_exit()
使用exit_group
系统调用而不是exit
系统调用的原因是exit_group
结束了整个过程。如果您在strace
下运行程序,则会在最后看到它也调用exit_group
syscall。
接下来,我们需要一些C代码。 main.c
:
// SPDX-License-Identifier: CC0-1.0
#include "nolib.h"
const char *c_function(void)
{
return "C function";
}
static inline long nolib_put(const char *msg)
{
if (!msg) {
return nolib_write(1, "(null)", 6);
} else {
const char *end = msg;
while (*end)
end++;
if (end > msg)
return nolib_write(1, msg, (unsigned long)(end - msg));
else
return 0;
}
}
extern const char *asm_function(int);
void _start(void)
{
nolib_put("asm_function(0) returns '");
nolib_put(asm_function(0));
nolib_put("', and asm_function(1) returns '");
nolib_put(asm_function(1));
nolib_put("'.\n");
nolib_exit(0);
}
nolib_put()
只是nolib_write()
的包装,它找到要写入的字符串的结尾,并以此为基础计算要写入的字符数。如果参数是NULL指针,则显示(null)
。
因为这是一个独立的环境,并且入口点的默认名称为_start
,所以这将_start
定义为永不返回的C函数。 (它绝不能返回,因为ABI不提供任何返回地址;它只会使进程崩溃。相反,必须在最后调用出口类型的syscall。)
C源代码声明并调用函数asm_function
,该函数采用整数参数,并返回指向字符串的指针。显然,我们将在汇编中实现它。
C源代码还声明了一个函数c_function
,我们可以从程序集中调用该函数。
这里是装配零件,asm.s
:
# SPDX-License-Identifier: CC0-1.0
.file "asm.c"
.text
.section .rodata
.one:
.string "One"
.text
.p2align 4,,15
.globl asm_function
.type asm_function, @function
asm_function:
cmpl $1, %edi
jne .else
leaq .one(%rip), %rax
ret
.else:
call c_function
ret
.size asm_function, .-asm_function
我们不需要将c_function
声明为extern,因为GNU as无论如何都将所有未知符号视为外部符号。我们可以添加control flow integrity directives,至少要添加.cfi_startproc
和.cfi_endproc
,但是我将它们省略了,这样就不太明显了,我只是用C编写了原始代码,然后让GCC将其编译为汇编,然后进行了美化一点点。 (我大声写出来了吗?糟糕!)
有了这四个文件,我们已经准备好了。要构建示例二进制文件,请运行例如
reset ; make clean all
运行./static-prog
或./dynamic-prog
将输出
asm_function(0) returns 'C function', and asm_function(1) returns 'One'.
这两个二进制文件的大小仅为2 kB(静态)和6 kB(动态),尽管您可以通过剥离不需要的东西使它们变得更小,
strip --strip-unneeded static-prog dynamic-prog
[它们从它们中删除了大约0.5 kB到1 kB的不需要的东西–确切的数量取决于所使用的GCC和Binutils的版本。
在其他一些架构上,我们还需要链接到libgcc(通过-lgcc
),因为某些C功能依赖于内部GCC功能。各种体系结构上的64位整数除法(命名为udivdi或类似名称)是一个典型示例。