您如何从汇编中调用C函数,以及如何将其静态链接?

问题描述 投票:2回答:1

我在四处游玩,并试图了解计算机和程序的低级操作。为此,我正在尝试将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内核下的所有体系结构都会为我照顾吗?

c linux assembly gcc x86-64
1个回答
2
投票

虽然Linux内核提供了一个名为write的系统调用,但这并不意味着您会自动获得同名的函数。实际上,您需要内联汇编才能从C调用任何系统调用。

而不是将二进制文件与ld明确链接,而是让gcc为您完成。如果源以as后缀结尾,它甚至可以编译程序集文件(内部执行.s的合适版本)。看起来您的链接问题只是GCC承担的假设与您自己通过LD进行操作之间的分歧。不,这不是错误。

Linux使用System V ABIAMD64 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或类似名称)是一个典型示例。

© www.soinside.com 2019 - 2024. All rights reserved.