如何更改解释器路径并将命令行参数传递给 Linux 上的 "executable" 共享库?

How to change interpreter path and pass command line arguments to an "executable" shared library on Linux?

这是一个 "executable" 共享库的最小示例(假设文件名:mini.c):

// Interpreter path is different on some systems
//+definitely different for 32-Bit machines

const char my_interp[] __attribute__((section(".interp"))) 
    = "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";

#include <stdio.h>
#include <stdlib.h>

int entry() {
    printf("WooFoo!\n");
    exit (0);
}

如果用例如 gcc -fPIC -o mini.so -shared -Wl,-e,entry mini.c 编译它。 "Running" 结果 .so 将如下所示:

confus@confusion:~$ ./mini.so
WooFoo!

我现在的问题是:
我如何更改上述程序以将命令行参数传递给 .so-文件的调用?更改后的示例 shell 会话可能例如看起来像这样:

confus@confusion:~$ ./mini.so 2 bar
1: WooFoo! bar!
2: WooFoo! bar!
confus@confusion:~$ ./mini.so 3 bla
1: WooFoo! bla!
2: WooFoo! bla!
3: WooFoo! bla!
5: WooFoo! Bar!

在编译时 检测目标是 32 位还是 64 位二进制文​​件 以相应地更改解释器字符串也很好。否则会收到 "Accessing a corrupted shared library" 警告。类似于:

#ifdef SIXTY_FOUR_BIT
    const char my_interp[] __attribute__((section(".interp"))) = "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";
#else
    const char my_interp[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.2";
#endif

或者甚至更好,完全自动检测适当的路径以确保它适合编译库的系统。

添加

int argc;
char **argv;

asm("mov 8(%%rbp), %0" : "=&r" (argc));
asm("mov %%rbp, %0\n"
    "add , %0"      : "=&r" (argv));

entry 函数的顶部。在 x86_64 平台上,这将使您能够访问参数。

LNW article that John Bollinger linked to in the comments explains why this code works. It might interest you why this is not required when you write a normal C program, or rather, why it does not suffice do just give your entry function the two usual int argc, char **argv arguments: The entry point for a C program normally is not the main function, but instead an assembler function by glibc that does some preparations for you - among others fetch the arguments from the stack - and that eventually (via some intermediate functions) calls your main function. Note that this also means that you might experience other problems, since you skip this initialization! For some history, the cdecl wikipedia page,尤其是 x86 和 x86_64 之间的区别,可能会引起更多兴趣。

How do I have to change the above program to pass command line arguments to a call of the .so-file?

当您 运行 您的共享库时,argcargv 将被传递给您在堆栈上的入口函数。

问题是在 x86_64 linux 上编译共享库时使用的调用约定将是 System V AMD64 ABI 的调用约定,它不接受参数堆栈但在寄存器中。

您需要一些 ASM 粘合代码,从堆栈中获取参数并将它们放入正确的寄存器中。

这是一个简单的 .asm 文件,您可以将其另存为 entry.asm 并仅 link :

global _entry
extern entry, _GLOBAL_OFFSET_TABLE_

section .text
BITS 64

_entry:
        mov rdi, [rsp]
        mov rsi, rsp
        add rsi, 8
        call .getGOT
.getGOT:
        pop rbx
        add rbx,_GLOBAL_OFFSET_TABLE_+$$-.getGOT wrt ..gotpc
        jmp entry wrt ..plt

该代码将参数从堆栈复制到适当的寄存器中,然后以与位置无关的方式调用您的 entry 函数。

然后您可以像编写常规 main 函数一样编写 entry

// Interpreter path is different on some systems
//+definitely different for 32-Bit machines

const char my_interp[] __attribute__((section(".interp")))
    = "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";

#include <stdio.h>
#include <stdlib.h>

int entry(int argc, char* argv[]) {
    printf("WooFoo! Got %d args!\n", argc);
    exit (0);
}

这就是您编译库的方式:

nasm entry.asm -f elf64
gcc -fPIC -o mini.so -shared -Wl,-e,_entry mini.c entry.o

优点是您不会将内联 asm 语句与您的 C 代码混在一起,而是您真正的入口点被干净地抽象在起始文件中。

It would also be nice to detect on compile time, wheter the target is a 32-Bit or 64-Bit binary to change the interpreter string accordingly.

不幸的是,no completely clean, reliable way to do that。您能做的最好的事情就是依赖您喜欢的具有正确定义的编译器。

因为你使用 GCC,你可以这样写你的 C 代码:

#if defined(__x86_64__)
    const char my_interp[] __attribute__((section(".interp")))
        = "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";
#elif defined(__i386__)
    const char my_interp[] __attribute__((section(".interp")))
        = "/lib/ld-linux.so.2";
#else
    #error Architecture or compiler not supported
#endif

#include <stdio.h>
#include <stdlib.h>

int entry(int argc, char* argv[]) {
    printf("%d: WooFoo!\n", argc);
    exit (0);
}

并且有两个不同的起始文件。
一个用于 64 位:

global _entry
extern entry, _GLOBAL_OFFSET_TABLE_

section .text
BITS 64

_entry:
        mov rdi, [rsp]
        mov rsi, rsp
        add rsi, 8
        call .getGOT
.getGOT:
        pop rbx
        add rbx,_GLOBAL_OFFSET_TABLE_+$$-.getGOT wrt ..gotpc
        jmp entry wrt ..plt

还有一个用于 32 位:

global _entry
extern entry, _GLOBAL_OFFSET_TABLE_

section .text
BITS 32

_entry:
        mov edi, [esp]
        mov esi, esp
        add esi, 4
        call .getGOT
.getGOT:
        pop ebx
        add ebx,_GLOBAL_OFFSET_TABLE_+$$-.getGOT wrt ..gotpc
        push edi
        push esi
        jmp entry wrt ..plt

这意味着您现在有两种稍微不同的方法来为每个目标编译您的库。

对于 64 位:

nasm entry.asm -f elf64
gcc -fPIC -o mini.so -shared -Wl,-e,_entry mini.c entry.o -m64

对于 32 位:

nasm entry32.asm -f elf32
gcc -fPIC -o mini.so -shared -Wl,-e,_entry mini.c entry32.o -m32

所以总而言之,您现在有两个起始文件 entry.asmentry32.asmmini.c 中的一组定义自动选择正确的解释器,两个略有不同根据目标编译库的方法。

因此,如果我们真的想一路走下去,剩下的就是创建一个 Makefile 来检测正确的目标并相应地构建您的库。
让我们这样做:

ARCH := $(shell getconf LONG_BIT)

all: build_$(ARCH)

build_32:
        nasm entry32.asm -f elf32
        gcc -fPIC -o mini.so -shared -Wl,-e,_entry mini.c entry32.o -m32

build_64:
        nasm entry.asm -f elf64
        gcc -fPIC -o mini.so -shared -Wl,-e,_entry mini.c entry.o -m64

我们到这里就完成了。只需 运行 make 来构建您的图书馆并让奇迹发生。