GCC 链接器:在指定部分移动符号
GCC linker: move a symbol in a specified section
可以将代码中的一些函数移动到特定的部分
在可执行文件上?如果可以,怎么做?
对于使用 gcc 编译的应用程序,我们有更多的源文件,包括
X.c。每个对象都是从关联的源代码编译的(X.o 从 X.c 获得)并且链接器生成一个大的可执行文件。
我需要 X.c 中的两个函数位于
可执行文件,比如 .magic_section。我想要这个的原因是
该部分将加载到与其余部分不同的另一个内存区域。
我的问题是我无法更改源 X.c,否则我会使用
一个特定的标志,例如 __attribute__ ((section ("magic_section")))
函数。
我在 linker 的文档中阅读了一些内容并为链接器编写了自定义脚本,但我未能指定必须在哪个部分放置特定符号。我只移动了一整节。
你可能会做的(不是很好,但理论上应该可行)是使用 --function-sections
和 --data-sections
,假设你的 GCC 版本/架构支持这些选项,然后使用链接器脚本手动调出需要进入给定文件的所有函数和变量。
这会创建类似于 .text.function_name
或 .data.variable_name
的部分。如果您熟悉通过 gcc 属性分配节,我假设您知道在链接器中做什么。
作为一个优势,如果您实际上不希望整个文件进入魔术部分,那么您可以选择函数。
不幸的是,如果不修改二进制 objects、动态 linker 或动态加载程序,您将无法完成此任务,无论如何,这是一项非常困难的任务。
选项 1 - ELF 操作
每个 ELF executable 由部分组成,其中包含实际的 code/data/symbol 字符串/......这个 ELF 公开的符号,它需要来自其他位置的符号,加载特定位置的位置 code/data,等等
您可以通过键入
来观察二进制文件中的段
readelf -l [your binary]
输出将类似于以下内容(我选择 ls 作为二进制文件):
[ishaypeled@ishay-dev bin]$ readelf -l --wide ./ls
Elf file type is EXEC (Executable file)
Entry point 0x4048bf
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8
INTERP 0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x01b694 0x01b694 R E 0x200000
LOAD 0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000864 0x0016d0 RW 0x200000
DYNAMIC 0x01be08 0x000000000061be08 0x000000000061be08 0x0001f0 0x0001f0 RW 0x8
NOTE 0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R 0x4
GNU_EH_FRAME 0x01895c 0x000000000041895c 0x000000000041895c 0x00071c 0x00071c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000210 0x000210 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
现在让我们检查一下这个输出:
第一个table(程序Headers):
[Type] - 段类型,这个段的用途是什么
[Offset] - 文件中此段开始的偏移量
[VirtAddr] - 我们要在进程地址 space 中加载此部分的位置(如果应该加载此段,则不会加载所有部分)
[PhysAddr] - 与我遇到的所有现代 OS 的 VirtAddr 相同
[FileSiz] - 这部分文件有多大。这是您的部分的 link - 当前部分包含 Offset 到 Offset+FileSiz
范围内的所有部分
[MemSiz] - 此部分在虚拟内存中有多大(这不必与文件中的大小相同!如果它跨越文件中的大小,超出部分设置为 0)
[Flg] - 权限标志,R-read E-execute W-write。
[对齐] - 内存中需要内存对齐。
您关注的是 LOAD (PT_LOAD) 类型的段。这些段将段中的数据分组,指示加载程序将它们放在进程地址中的什么位置 space 并确定指定它们的权限。
您可以在部分到段映射中看到方便的部分到段映射 table。
让我们观察两个 LOAD 段 2 和 3:
我们可以看到段 2 具有读取和执行权限,并且它跨越(除其他外).text 和 .rodata 部分。
因此,要使用 ELF 操作实现您的目的:
- 在文件中找到生成您的函数的二进制数据(readelf 实用程序是您的朋友)
- 通过修改 ELF header(我不知道有什么工具可以自动执行此操作,您可能必须自己编写)将包含 .text 部分的段分成两个连续的 LOAD 段,省略你的函数代码
- 通过修改 ELF header 创建一个仅包含您的两个函数的新 LOAD 段
- 将对旧函数位置的所有引用(如果有)更新到新函数位置
如果你读到这里并理解了所有内容,你应该知道这对于现实生活中的案例来说是一项极其乏味、几乎不可能完成的任务。
选项 2 - 动态 linker 操纵
请注意上例中的 INTERP 段类型。这是一个 ASCII 字符串,指定您应该使用哪个动态 linker。
动态 linker 的作用是解析段并执行所有动态操作,例如在运行时解析符号、从 .so 文件加载段等。
此处可能的操作是修改动态 linker 代码(注意:这是一个系统范围的更改!)以将函数二进制数据加载到进程地址中的特定内存地址 space。请注意,这种方法有几个缺点:
- 需要修改动态linker
- 您仍然需要更新 ELF 文件中对您的函数的所有引用
选项 3 - 动态加载程序操作
很像选项 2,但修改 ld 库设施而不是动态 linker.
结论
正是你想做的事情非常困难,而且确实是一项乏味的任务。我目前正在开发一种工具,允许将任意函数注入现有的共享 object 文件,我保证这至少需要几个星期的工作。
您确定没有其他方法可以实现您想要的吗?为什么需要这两个函数在一个单独的地址?也许有更简单的解决方案...
可以将代码中的一些函数移动到特定的部分 在可执行文件上?如果可以,怎么做?
对于使用 gcc 编译的应用程序,我们有更多的源文件,包括 X.c。每个对象都是从关联的源代码编译的(X.o 从 X.c 获得)并且链接器生成一个大的可执行文件。
我需要 X.c 中的两个函数位于 可执行文件,比如 .magic_section。我想要这个的原因是 该部分将加载到与其余部分不同的另一个内存区域。
我的问题是我无法更改源 X.c,否则我会使用
一个特定的标志,例如 __attribute__ ((section ("magic_section")))
函数。
我在 linker 的文档中阅读了一些内容并为链接器编写了自定义脚本,但我未能指定必须在哪个部分放置特定符号。我只移动了一整节。
你可能会做的(不是很好,但理论上应该可行)是使用 --function-sections
和 --data-sections
,假设你的 GCC 版本/架构支持这些选项,然后使用链接器脚本手动调出需要进入给定文件的所有函数和变量。
这会创建类似于 .text.function_name
或 .data.variable_name
的部分。如果您熟悉通过 gcc 属性分配节,我假设您知道在链接器中做什么。
作为一个优势,如果您实际上不希望整个文件进入魔术部分,那么您可以选择函数。
不幸的是,如果不修改二进制 objects、动态 linker 或动态加载程序,您将无法完成此任务,无论如何,这是一项非常困难的任务。
选项 1 - ELF 操作
每个 ELF executable 由部分组成,其中包含实际的 code/data/symbol 字符串/......这个 ELF 公开的符号,它需要来自其他位置的符号,加载特定位置的位置 code/data,等等
您可以通过键入
来观察二进制文件中的段readelf -l [your binary]
输出将类似于以下内容(我选择 ls 作为二进制文件):
[ishaypeled@ishay-dev bin]$ readelf -l --wide ./ls
Elf file type is EXEC (Executable file)
Entry point 0x4048bf
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8
INTERP 0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x01b694 0x01b694 R E 0x200000
LOAD 0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000864 0x0016d0 RW 0x200000
DYNAMIC 0x01be08 0x000000000061be08 0x000000000061be08 0x0001f0 0x0001f0 RW 0x8
NOTE 0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R 0x4
GNU_EH_FRAME 0x01895c 0x000000000041895c 0x000000000041895c 0x00071c 0x00071c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000210 0x000210 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
现在让我们检查一下这个输出:
第一个table(程序Headers):
[Type] - 段类型,这个段的用途是什么
[Offset] - 文件中此段开始的偏移量
[VirtAddr] - 我们要在进程地址 space 中加载此部分的位置(如果应该加载此段,则不会加载所有部分)
[PhysAddr] - 与我遇到的所有现代 OS 的 VirtAddr 相同
[FileSiz] - 这部分文件有多大。这是您的部分的 link - 当前部分包含 Offset 到 Offset+FileSiz
范围内的所有部分
[MemSiz] - 此部分在虚拟内存中有多大(这不必与文件中的大小相同!如果它跨越文件中的大小,超出部分设置为 0)
[Flg] - 权限标志,R-read E-execute W-write。
[对齐] - 内存中需要内存对齐。
您关注的是 LOAD (PT_LOAD) 类型的段。这些段将段中的数据分组,指示加载程序将它们放在进程地址中的什么位置 space 并确定指定它们的权限。
您可以在部分到段映射中看到方便的部分到段映射 table。
让我们观察两个 LOAD 段 2 和 3:
我们可以看到段 2 具有读取和执行权限,并且它跨越(除其他外).text 和 .rodata 部分。
因此,要使用 ELF 操作实现您的目的:
- 在文件中找到生成您的函数的二进制数据(readelf 实用程序是您的朋友)
- 通过修改 ELF header(我不知道有什么工具可以自动执行此操作,您可能必须自己编写)将包含 .text 部分的段分成两个连续的 LOAD 段,省略你的函数代码
- 通过修改 ELF header 创建一个仅包含您的两个函数的新 LOAD 段
- 将对旧函数位置的所有引用(如果有)更新到新函数位置
如果你读到这里并理解了所有内容,你应该知道这对于现实生活中的案例来说是一项极其乏味、几乎不可能完成的任务。
选项 2 - 动态 linker 操纵
请注意上例中的 INTERP 段类型。这是一个 ASCII 字符串,指定您应该使用哪个动态 linker。
动态 linker 的作用是解析段并执行所有动态操作,例如在运行时解析符号、从 .so 文件加载段等。
此处可能的操作是修改动态 linker 代码(注意:这是一个系统范围的更改!)以将函数二进制数据加载到进程地址中的特定内存地址 space。请注意,这种方法有几个缺点:
- 需要修改动态linker
- 您仍然需要更新 ELF 文件中对您的函数的所有引用
选项 3 - 动态加载程序操作 很像选项 2,但修改 ld 库设施而不是动态 linker.
结论
正是你想做的事情非常困难,而且确实是一项乏味的任务。我目前正在开发一种工具,允许将任意函数注入现有的共享 object 文件,我保证这至少需要几个星期的工作。
您确定没有其他方法可以实现您想要的吗?为什么需要这两个函数在一个单独的地址?也许有更简单的解决方案...