在Linux上为x64编写C和汇编自定义加载程序

3
我希望能够在x64 Linux上编写自己的二进制代码加载器。将来,我想能够自己执行链接步骤,从而能够调用来自.o对象文件的代码。但现在,我想调用已经链接的可执行二进制文件中的函数。
为了创建一些应该可以从“外部”调用的函数,我从以下源代码开始:
void foo(void)
{
  int a = 2;
  int b = 3;
  a + b;
}

int main(void)
{
  foo();
  return 0;
}

我想使用我的加载器来调用foo()函数。可以使用以下命令链:

gcc -o /tmp/main main.c
strip -s /tmp/main
objdump -D /tmp/main

我得到了foo()函数的汇编代码,如下所示:

...
0000000000001125 <foo>:
    1125:   55                      push   %rbp
    1126:   48 89 e5                mov    %rsp,%rbp
    1129:   c7 45 fc 02 00 00 00    movl   $0x2,-0x4(%rbp)
    1130:   c7 45 f8 03 00 00 00    movl   $0x3,-0x8(%rbp)
    1137:   90                      nop
    1138:   5d                      pop    %rbp
    1139:   c3                      retq
...

这意味着foo()函数在main中的偏移量为0x1125。我使用十六进制编辑器进行了验证。
以下是我的加载器。目前还没有错误处理,代码非常丑陋。但是,它应该可以演示我想要实现的内容:
#include <stdio.h>
#include <stdlib.h>

typedef void(*voidFunc)(void);

int main(int argc, char* argv[])
{
  FILE *fileptr;
  char *buffer;
  long filelen;
  voidFunc mainFunc;

  fileptr = fopen(argv[1], "rb");  // Open the file in binary mode
  fseek(fileptr, 0, SEEK_END);          // Jump to the end of the file
  filelen = ftell(fileptr);             // Get the current byte offset in the file
  rewind(fileptr);                      // Jump back to the beginning of the file

  buffer = (char *)malloc((filelen+1)*sizeof(char)); // Enough memory for file + \0
  fread(buffer, filelen, 1, fileptr); // Read in the entire file
  fclose(fileptr); // Close the file

  mainFunc = ((voidFunc)(buffer + 0x1125));

  mainFunc();

  free(buffer);

  return 0;
}

执行该程序objloader /tmp/main时,会导致 SEGFAULT 错误。

mainFunc 变量指向正确的位置,我使用 gdb 进行了验证。

堆上存储操作码是否有问题?实际上,我决定让我想调用的函数尽可能简单(不需要使用栈或寄存器来传递参数等副作用)。但是,仍然有一些我真正不理解的问题。

请问有人可以指点我正确的方向吗?对于这方面有用的文献提示也将不胜感激!


为什么你想要加载未链接的对象文件?那没有意义。你是不是想要加载以.so结尾的动态库 - Some programmer dude
2
是的,您需要将内存标记为可执行。最简单的方法是使用 mmap,而不是 malloc/fread。此外,如果您无法保证加载地址(这个是可以的),则代码应该是位置无关的。您可能还想看一下 dlopen - Jester
在以下情况下调用未链接的目标文件可能是有意义的:clang/LLVM可以为大量平台生成代码。如果存在某些需要花费$$$的编译器/链接器,那么我可能可以自己进行链接/加载,并因此使用$$$编译器/链接器创建加载程序,但实际上执行一些已使用免费clang/LLVM编译的其他代码。 - dubbaluga
3
你正在将可执行文件读入内存,并尝试在文件中的偏移量0x1125处执行,因为foo在代码段中的偏移量为0x1125。但是可执行文件不仅仅是程序或其代码在内存中的形象,它是一个结构化文件。在开头有说明文件中包含了什么内容,以及有多个节(section)。每个节都有说明它是什么类型、长度等信息。还有关于可重定位符号等的信息。要加载可执行文件,必须解析和处理文件内容。 - Eric Postpischil
是的,我知道有几个部分。但在这种情况下,该函数不应依赖于任何其他部分。我仍然希望它可以直接调用。 - dubbaluga
显示剩余6条评论
2个回答

5
为了使内存区域buffer可执行,您需要使用mmap。请尝试。
#include <sys/mman.h>
...
buffer = (char *)mmap(NULL, filelen /* + 1? Not sure why. */, PROT_EXEC | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

这样可以给内存区域赋予所需的权限并使其与周围的代码配合工作。事实上,如果你想要按照设计意图使用mmap,请这样做。

int fd = open(argv[1], O_RDONLY);
struct stat myfilestats;
fstat(fd, &myfilestats);
buffer = (char*)mmap(NULL, myfilestats.st_size, PROT_EXEC, MAP_PRIVATE, fd, 0);
fclose(fd);
...
munmap(buffer, myfilestats.st_size);

使用MAP_ANONYMOUS将使内存区域与文件描述符无关联,但是如果它表示一个文件,则应该将文件描述符与其关联。当您这样做时,Linux会执行各种酷炫的技巧,例如仅加载您实际访问的文件部分(延迟加载也会使程序在文件很大时非常平滑),如果多个程序都访问同一个文件,则它们将共享相同的物理内存位置。

谢谢,这正是我想要的。我会在上面发布我的“加载器”的最终版本。 - dubbaluga

1

这是我的“加载器”最终版本,基于Nicholas Pipiton的回答。同样:没有错误处理、简化了问题,没有考虑到现实世界场景要困难的多等等。

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <stdlib.h>

typedef void(*voidFunc)(void);

int main(int argc, char* argv[])
{
  char* buffer;
  voidFunc mainFunc;
  struct stat myfilestats;
  int fd;

  fd = open(argv[1], O_RDONLY);
  fstat(fd, &myfilestats);
  buffer = mmap(NULL, myfilestats.st_size, PROT_EXEC, MAP_PRIVATE, fd, 0);
  close(fd);

  mainFunc = ((voidFunc)(buffer + 0x1125));

  mainFunc();

  munmap(buffer, myfilestats.st_size);

  return EXIT_SUCCESS;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接