C语言 从ELF中提取符号表到a .o

vs91vp4v  于 2023-08-03  发布在  其他
关注(0)|答案(2)|浏览(149)

我正在为一个通过串行连接的设备编写一个裸机应用程序。设备上已经有一些基本代码(可能被认为是操作系统),但我希望能够在我的计算机上交叉编译代码,通过串行发送机器码,并让设备将机器码加载到内存中并运行它。我对安全性没有任何真实的的需求,因此通过串行发送的基本代码和机器码都在管理模式下运行(这是一个ARM设备),并且可以访问整个物理内存而无需分页。所有有问题的代码都是用C编写的(还有一些可以忽略的汇编)。
我唯一的问题是,我希望新编译的机器码能够调用在基本代码中定义的子程序。例如,基本代码有一个printf函数,它会写入显示器,我希望能够从通过串行发送的代码中调用该printf函数。我想我也许可以将printf函数从基本代码中提取出来,并将其放入可重用的库中,但是,设备内存有限,所以我不想静态链接它,我认为动态链接器是多余的。
我目前的想法是让通过串行发送的代码与基本代码的符号一起编译。两者最终都在同一台机器上运行,所以不应该有任何问题。当前的构建过程将我的C代码与Clang交叉编译为ELF,然后使用objcopy code.elf -O binary code.img提取机器码。唯一的问题是编译后通过串行发送的代码无法访问基本代码的符号。我需要从base.elf中提取符号表,并在链接serial_code.o时传递它。
我该怎么办?最好使用LLVM实用程序。

omqzjyyz

omqzjyyz1#

我没能得到一个能用objcopy工作的解决方案,但是这个方法只需要readelf和一些脚本语言(我使用Python)。我假设基本代码的ELF名为base.elf
在未剥离的ELF文件中,符号表存储在.symtab部分。因此,要读取符号表,您所要做的就是读取带有readelf -s base.elf的部分。输出如下所示(这是一个简化的示例,您的表可能会有所不同):

Symbol table '.symtab' contains 5 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS uart1.c
     2: 0000832c   100 FUNC    LOCAL  DEFAULT    3 other_function1
     3: 000083c8    48 FUNC    LOCAL  DEFAULT    3 other_function2
     4: 000081dc   336 FUNC    GLOBAL DEFAULT    3 uart1_init
     5: 00009000    12 OBJECT  GLOBAL DEFAULT    6 uart1

字符串
我们所关心的是全局符号,因此您可以将此输出通过管道传输到grep,并仅过滤全局符号。如果你想限制哪些符号被复制,你可以做更多的过滤,但这取决于你。
获得符号列表后,可以创建将每个符号定义为绝对地址的部件文件。此程序集文件中的每个条目看起来如下所示:

.global uart1_init
.set uart1_init, 0x000081dc


手工处理大量的符号会很快变得非常累人,所以我想出了一个Python脚本来自动将readelf输出转换为汇编文件:

from os import popen
from sys import argv

symbols = popen(f'readelf -s {argv[1]} | grep GLOBAL | rev | cut -d" " -f1 | rev').read().splitlines()
addrs = popen(f'readelf -s {argv[1]} | grep GLOBAL | cut -d" " -f6').read().splitlines()

with open(argv[2], 'w') as f:
    for (symbol, addr) in zip(symbols, addrs):
        f.write(f'.global {symbol}\n')
        f.write(f'.set {symbol}, 0x{addr}\n')


(The脚本接受两个命令行参数:输入ELF文件和输出程序集源的文件名)。
一旦获得了包含所需所有符号的汇编文件,就需要使用所选的汇编程序将其汇编成一个对象文件。如果你使用的是clang,那么你可以使用这个命令(任何指定ELF格式的目标都可以工作):

clang --target=YOUR_TARGET -c -o symbols.o symbols.S


这个目标文件将包含所有你想要的符号,现在你可以把它传递给你的链接器,符号将被解析为来自原始基ELF的地址。
请注意,您仍然需要定义在C中使用的所有符号,以使编译器满意。这可以通过使用extern定义或简单地包含base.elf源代码中的头文件来完成。

suzh9iv8

suzh9iv82#

您不需要提取符号表。您需要将代码与基础代码中已使用的代码符号链接起来。您可以正常链接基本代码(没有未满足引用的代码),然后将模块链接为共享对象(动态链接模块),所有链接都将在共享对象加载时完成。
这是做这件事的一般方法。如果你尝试自己做,通过探索基本可执行文件来定位符号并在运行时解析引用,你将在链接时做动态链接器正在做的事情,所以你的代码将增长(但不像动态链接器那样优化)到它实际拥有的大小。你是在重新发明轮子,在你得到一些可交付的东西之前,你可能会有很长的调试期。
我建议你阅读链接器ld如何工作的完整文档(编写链接器脚本的方式),因为它提供了比我上面描述的更多的可能性。
另一种可能性是创建一个函数入口点表,并在某个全局变量中对其进行初始化。你只需要访问基本软件提供的函数的指针,这样你就可以从存储在全局变量中的指针访问入口点。

extern struct GLOBAL_ENTRY_POINTS {
     void function_a(int param_1, double param_2);
     ...
};

struct GLOBAL_ENTRY_POINTS global_entry_points = {  .function_a = function_a_impl, ... };

字符串
然后,在代码中,可以通过某种外部方式给出该全局变量的位置,并调用

global_entry_points.function_a(3, 3.141592654);


当你的代码被加载到内存中。
这种方法不需要任何计算,因为主代码的地址在调用链接器时就已经分配好了。并且一旦您获得了该全局变量的地址就可以访问。(所有这些都可以在构建时静态地确定,因此您不需要在运行时做任何事情)。

相关问题