ELF entry point和装载地址

本文深入探讨了ELF文件如何加载到内存及程序的执行起点,通过实例介绍了_start函数的作用及其如何被替代,同时讲解了如何指定程序入口地址及ELF文件的加载基址。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为了研究ELF文件装载到内存的哪里,以及从哪里开始运行程序,环境:ubuntu12.04 64位,gcc4.6.3。

使用的源代码是:

#include <stdlib.h>

void hello(void)
{
        exit(42);
}

int main(void)
{
        return 24;
}

程序并不是从main函数开始执行的,gcc -o main main.c时,默认会连接libc.so(可以指定-nodefaultlib, -nostdlib取消连接),并且会添加一个启动代码_start函数(可以指定-nodefaultlib, -nostdlib不添加启动代码),用于初始化,并提供main函数的argc, argv等参数,_start函数中会调用main函数。

readelf -d main可以看到,程序链接了libc.so.6:

$ readelf -d main

Dynamic section at offset 0xe50 contains 20 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x400390
 0x000000000000000d (FINI)               0x4005c8

objdump -d main可以看到,程序中有个_start函数,_start的地址是4003d0:

$ objdump -d main | grep _start
  400394:	e8 63 00 00 00       	callq  4003fc <call_gmon_start>
00000000004003b0 <__libc_start_main@plt-0x10>:
00000000004003c0 <__libc_start_main@plt>:
00000000004003d0 <_start>:
  4003f4:	e8 c7 ff ff ff       	callq  4003c0 <__libc_start_main@plt>
readelf -h main | grep Entry可以看到:

  Entry point address:               0x4003d0
程序的入口地址是0x4003d0,也就是_start函数的地址,程序装载到内存后,从0x4003d0(_start)开始执行。


那么,我不想执行_start函数呢,可以通过ld的参数-e指定入口函数,使用gcc -o main mian.c -Wl,-ehello编译,-Wl用于指定后面的参数是给ld的,-e指定入口函数是hello。

注意hello不能直接return,因为返回地址是错误的,会导致coredump;hello应当使用exit()函数退出程序。./main得到:

$ ./main
$ echo $?
42
objdump -d main | grep hello查看hello的地址:

$ objdump -d main | grep hello
00000000004004f4 <hello>:
readelf -h main | grep Entry查看程序的入口地址:

$ readelf -h main | grep Entry
  Entry point address:               0x4004f4
证明了-Wl,-ehello可以指定程序的入口函数为hello。



那么程序的入口地址0x4004f4是怎么来的呢?

0x4f4其实是hello函数在ELF文件中的偏移量,可以通过hexdump -C main | grep -A5 -B5 4f0查看到

$ hexdump -C main | grep -A5 -B5 4f0
000004a0  0b 20 00 ff 14 c5 38 0e  60 00 48 8b 05 77 0b 20  |. ....8.`.H..w. |
000004b0  00 48 39 d8 72 e2 c6 05  63 0b 20 00 01 48 83 c4  |.H9.r...c. ..H..|
000004c0  08 5b 5d c3 66 66 66 2e  0f 1f 84 00 00 00 00 00  |.[].fff.........|
000004d0  48 83 3d 70 09 20 00 00  55 48 89 e5 74 12 b8 00  |H.=p. ..UH..t...|
000004e0  00 00 00 48 85 c0 74 08  5d bf 48 0e 60 00 ff e0  |...H..t.].H.`...|
000004f0  5d c3 90 90 <span style="color:#ff0000;">55 48 89 e5  bf 2a 00 00 00 e8 fe fe</span>  |]...UH...*......|
00000500  <span style="color:#ff0000;">ff ff</span> 55 48 89 e5 b8 18  00 00 00 5d c3 90 90 90  |..UH.......]....|
00000510  48 89 6c 24 d8 4c 89 64  24 e0 48 8d 2d 03 09 20  |H.l$.L.d$.H.-.. |
00000520  00 4c 8d 25 fc 08 20 00  4c 89 6c 24 e8 4c 89 74  |.L.%.. .L.l$.L.t|
00000530  24 f0 4c 89 7c 24 f8 48  89 5c 24 d0 48 83 ec 38  |$.L.|$.H.\$.H..8|
00000540  4c 29 e5 41 89 fd 49 89  f6 48 c1 fd 03 49 89 d7  |L).A..I..H...I..|
然后objdump -d main | grep -A5 -B5 hello查看hello对应的机器码:

$ objdump -d main | grep -A5 -B5 hello
  4004f0:	5d                   	pop    %rbp
  4004f1:	c3                   	retq
  4004f2:	90                   	nop
  4004f3:	90                   	nop

00000000004004f4 <hello>:
  4004f4:	<span style="color:#ff0000;">55</span>                   	push   %rbp
  4004f5:	<span style="color:#ff0000;">48 89 e5</span>             	mov    %rsp,%rbp
  4004f8:	<span style="color:#ff0000;">bf 2a 00 00 00</span>       	mov    $0x2a,%edi
  4004fd:	<span style="color:#ff0000;">e8 fe fe ff ff</span>       	callq  400400 <exit@plt>
证明了这一点。

然后0x400000是什么呢?

这个是ELF装载到内存时的起始位置,可以通过ld的参数-Wl,-Ttext-segment,0x400000指定,比如我们gcc -o main main.c -Wl,-ehello -Wl,-Ttext-segment,0x4200000编译程序,给程序换一个装载地址,然后readelf -h main | grep Entry得到:

Entry point address:               0x42004f4

hello在文件中的偏移依然为0x4f4,然后加上装载地址0x4200000就可以得到程序执行的入口地址:0x42004f4。

objdump -d main | grep hello -A 10可以看到装载地址变了,程序的PC指针位置也都改变了(因为我们编译的main是地址相关的ELF):

$ objdump -d main | grep hello -A 10
00000000042004f4 <hello>:
 <span style="color:#ff0000;background-color: rgb(204, 204, 204);">42004f4</span>:	55                   	push   %rbp
 <span style="color:#ff0000;">42004f5</span>:	48 89 e5             	mov    %rsp,%rbp
 <span style="color:#ff0000;">42004f8</span>:	bf 2a 00 00 00       	mov    $0x2a,%edi
 <span style="color:#ff0000;">42004fd</span>:	e8 fe fe ff ff       	callq  4200400 <exit@plt>

0000000004200502 <main>:
 4200502:	55                   	push   %rbp
 4200503:	48 89 e5             	mov    %rsp,%rbp
 4200506:	b8 18 00 00 00       	mov    $0x18,%eax
 420050b:	5d                   	pop    %rbp


注意

1、-Ttext-segment指定的必需是一个页对齐的地址。

2、动态库的装载位置不是固定的,一般可以认为动态库的-Ttext-segment=0,没有使用;如果想直接运行.so的话(glibc里面很多.so都可以直接运行),需要为.so指定ld-linux.so(.so默认没有指定):
gcc -shared -fPIC -o libx.so x.c -Wl,--dynamic-linker=some_ld_linux.so.x
或者代码里面写:const char my_interp[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.3"; //需要保证这个ld-linux.so存在且能用

可以使用readelf -l查看.inter段,也就是指定的ld-linux.so

3、-Ttext-segment并不是指定.text段的加载位置,而是指定整个elf的加载位置。

如果想指定.text段的加载位置,可以:
       -Tbss=org
       -Tdata=org
       -Ttext=org
           Same as --section-start, with ".bss", ".data" or ".text" as the
           sectionname.
4、gcc -Wl,-Ttext-segment=0x400000 -Wl,-Ttext=0x800000  -o x x.c这样是可以的,但是如果-Ttext指定的比较小,要么程序无法运行,要么可能和其他段冲突。加完-Ttext之后,.text段以及后面的段,在文件中的偏移会变大不少,中间填充的0是为了加载到内存后的页对齐,在内存中的加载位置都会从-Ttext指定的位置开始。

<think>嗯,用户问的是关于GCC编译时如何确定ELF文件的入口地址entry point)。我需要先回忆一下ELF文件的结构以及链接过程的相关知识。 首先,ELF文件的入口地址是由链接器决定的,而链接器使用的链接脚本(linker script)在这里起着关键作用。链接脚本里有一个ENTRY()命令,用来指定入口点符号,比如ENTRY(_start)。这个符号通常是程序的启动函数,比如在C语言中,可能对应着_start或者main,但具体取决于运行时库的实现。 不过用户可能想知道的是这个入口地址具体是如何确定的,特别是在默认情况下,当没有指定ENTRY时,链接器会怎么处理。我记得链接器会按照一定的优先级来选择入口点,比如如果存在_start符号,就会优先使用它;如果没有,可能会尝试使用其他默认符号,比如main。但需要确认这一点是否正确。 另外,GCC在编译链接的时候,会使用默认的链接脚本,这个脚本里可能已经定义了ENTRY(_start)。用户可能想知道如何查看默认的链接脚本,以及如何自定义入口点。这时候需要提到GCC的-Wl,-verbose选项,可以输出默认的链接脚本内容。 还有,关于入口地址的具体数值,这取决于链接时各个段的布局。比如,代码段(.text)的起始地址会影响入口点的最终值。可能涉及到可执行文件是位置无关的(PIE)还是非位置无关的情况,这时候入口地址可能是一个绝对地址或者相对地址。 用户可能在实际开发中遇到入口点设置不正确的问题,比如链接了多个启动文件导致入口点冲突,或者自定义的启动函数没有被正确识别。这时候需要解释如何通过修改链接脚本或者使用GCC的-e选项来指定入口点。 需要确保步骤清晰,先讲默认行为,再讲如何查看修改入口点。可能还需要给出例子,比如使用gcc的-e参数指定入口函数,或者修改链接脚本中的ENTRY命令。 最后,验证一下提到的内容是否正确。比如,检查默认的入口点符号是否确实是_start,以及如何通过readelf命令查看ELF文件的入口地址。可能需要提醒用户使用readelf -h来查看入口点地址,以及objdump查看符号地址是否正确。</think>GCC编译确定ELF文件入口地址的过程可分为以下步骤: 一、核心概念解析 1. ELF入口地址(Entry Point)是操作系统加载程序后第一条指令的执行地址 2. 由链接器(ld)最终确定,受多个因素共同影响 二、确定机制 1. 链接脚本优先级(最高): - 通过`ENTRY(symbol)`指令显式指定 - 默认链接脚本通常包含`ENTRY(_start)` 2. 符号优先级(次之): ```bash $ ld --verbose | grep ENTRY # 查看默认入口设置 ENTRY(_start) ``` 若未指定ENTRY(),链接器按以下顺序选择: - `_start`符号(常见于crt0.o) - `.text`段起始地址 - 地址0x0 三、验证方法 1. 使用readelf工具查看: ```bash $ readelf -h a.out | grep Entry Entry point address: 0x400430 ``` 2. 符号地址映射: ```bash $ objdump -d a.out | grep -A 10 "<_start>:" 0000000000400430 <_start>: 400430: 31 ed xor %ebp,%ebp ``` 四、自定义入口地址 1. 编译时指定: ```bash gcc -Wl,-eentry_func -nostartfiles main.c ``` 2. 修改链接脚本: ```ld ENTRY(my_entry) SECTIONS { . = 0x400000; .text : { *(.text) } } ``` 五、注意事项 1. 使用`-nostdlib`时需手动指定入口 2. 位置无关代码(PIE)会使入口地址变为相对地址 3. 动态链接程序实际入口位于解释器(ld-linux.so) 通过理解这些机制,开发者可以精准控制程序启动流程,这对内核开发、引导程序等底层开发尤为重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值