链接

话不多说,先看例子

test.c

#include <stdio.h>

int a = 100;

void main()
{
    printf("main = %p\n", main);
    printf("&a = %p\n", &a);
    printf("a = %d\n", a);
    *(int *)0x4000010 = 20;
    printf("a = %d\n", a);
}

test.lds

SECTIONS
{
    .text 0x30000000:
    {
        *(.text)
    }
    
    .data 0x4000000:
    {
        *(.data)
    }
    
    .bss :
    {
        *(.bss)
    }
}

$ gcc test.c test.lds -o test -no-pie
$ ./test 
main = 0x300000e6
&a = 0x4000010
a = 100
a = 20

位置无关码 & 位置有关码

PIE(position-independent executable,位置无关码),no-pie 就是位置有关码。

PIE 还有个孪生兄弟 PIC(position-independent code)。其作用和 PIE 相同,都是使被编译后的程序能够随机的加载到某个内存地址。区别在于 PIC 是在生成动态链接库时使用(Linux 中的 so),PIE 是在生成可执行文件时使用。

链接地址 ≠ 运行地址:位置无关码

链接地址 = 运行地址:位置有关码

示例解释

上述示例,编译时使用了 -no-pie 选项,故编译出来的代码是位置有关码,即,链接地址 = 运行地址。我们来检查下是否符合预期。

通过 readelf -h test 看到,程序的入口地址为 0x30000000,和我们链接脚本中的 .text 0x30000000 一致。

$ readelf -h test
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x30000000
  程序头起点:          64 (bytes into file)
  Start of section headers:          22848 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         15
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

反汇编看到,main 函数的地址为 0x300000e6,和打印一致,全局变量 a 的地址为 0x4000010,和打印一致。

$ objdump -d test

test:     文件格式 elf64-x86-64

...

Disassembly of section .text:

0000000030000000 <_start>:
    30000000:   f3 0f 1e fa             endbr64 
    30000004:   31 ed                   xor    %ebp,%ebp
    30000006:   49 89 d1                mov    %rdx,%r9
    30000009:   5e                      pop    %rsi
    3000000a:   48 89 e2                mov    %rsp,%rdx
    3000000d:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
    30000011:   50                      push   %rax
    30000012:   54                      push   %rsp
    30000013:   49 c7 c0 d0 01 00 30    mov    $0x300001d0,%r8
    3000001a:   48 c7 c1 60 01 00 30    mov    $0x30000160,%rcx
    30000021:   48 c7 c7 e6 00 00 30    mov    $0x300000e6,%rdi
    30000028:   ff 15 c2 2f 00 00       callq  *0x2fc2(%rip)        # 30002ff0 <__libc_start_main@GLIBC_2.2.5>
    3000002e:   f4                      hlt    
    3000002f:   90                      nop

...

00000000300000e6 <main>:
    300000e6:   f3 0f 1e fa             endbr64 
    300000ea:   55                      push   %rbp
    300000eb:   48 89 e5                mov    %rsp,%rbp
    300000ee:   48 8d 35 f1 ff ff ff    lea    -0xf(%rip),%rsi        # 300000e6 <main>
    300000f5:   48 8d 3d 08 0f 00 00    lea    0xf08(%rip),%rdi        # 30001004 <_IO_stdin_used+0x4>
    300000fc:   b8 00 00 00 00          mov    $0x0,%eax
    30000101:   e8 3a 0f 40 d0          callq  401040 <printf@plt>
    30000106:   48 8d 35 03 ff ff d3    lea    -0x2c0000fd(%rip),%rsi        # 4000010 <a>
    3000010d:   48 8d 3d fb 0e 00 00    lea    0xefb(%rip),%rdi        # 3000100f <_IO_stdin_used+0xf>
    30000114:   b8 00 00 00 00          mov    $0x0,%eax
    30000119:   e8 22 0f 40 d0          callq  401040 <printf@plt>
    3000011e:   8b 05 ec fe ff d3       mov    -0x2c000114(%rip),%eax        # 4000010 <a>
    30000124:   89 c6                   mov    %eax,%esi
    30000126:   48 8d 3d eb 0e 00 00    lea    0xeeb(%rip),%rdi        # 30001018 <_IO_stdin_used+0x18>
    3000012d:   b8 00 00 00 00          mov    $0x0,%eax
    30000132:   e8 09 0f 40 d0          callq  401040 <printf@plt>
    30000137:   b8 10 00 00 04          mov    $0x4000010,%eax
    3000013c:   c7 00 14 00 00 00       movl   $0x14,(%rax)
    30000142:   8b 05 c8 fe ff d3       mov    -0x2c000138(%rip),%eax        # 4000010 <a>
    30000148:   89 c6                   mov    %eax,%esi
    3000014a:   48 8d 3d c7 0e 00 00    lea    0xec7(%rip),%rdi        # 30001018 <_IO_stdin_used+0x18>
    30000151:   b8 00 00 00 00          mov    $0x0,%eax
    30000156:   e8 e5 0e 40 d0          callq  401040 <printf@plt>
    3000015b:   90                      nop
    3000015c:   5d                      pop    %rbp
    3000015d:   c3                      retq   
    3000015e:   66 90                   xchg   %ax,%ax
...

由于是位置有关码,运行地址 = 链接地址,我们能够很清楚地知道在运行时代码所处的地址,全局变量所处的地址。在示例代码中,我们将 data 段的地址设置为了 0x4000000,则全局变量 a 就落在这个区间,为 0x4000010,这样,甚至,我们可以在代码中直接操作这个地址,将其储存的值 100 改为 20,也就是示例中的效果。

位置无关码

test.c

#include <stdio.h>

int a = 100;

void main()
{
    printf("main = %p\n", main);
    printf("&a = %p\n", &a);
    // printf("a = %d\n", a);
    // *(int *)0x4000010 = 20;
    // printf("a = %d\n", a);
}

test.lds 和示例一保持不变

去除位置有关码编译选项,编译得到位置无关码,运行

$ gcc test.c test.lds -o test
liyongjun@Box:~/project/c/ld/4$ ./test 
main = 0x564ec6e1e0e9
&a = 0x564e9ae1e010
liyongjun@Box:~/project/c/ld/4$ ./test 
main = 0x55a23683f0e9
&a = 0x55a20a83f010
liyongjun@Box:~/project/c/ld/4$ ./test 
main = 0x558bf273a0e9
&a = 0x558bc673a010

可以看到,每次运行,运行地址都不一样,都是加载器随机选择内存的。

位置无关码介绍

因为可执行程序的代码段只有读和执行属性没有写属性,而数据段具有读写属性。要实现地址无关代码,就要将代码段中需要改变的值分离到数据段中,而程序加载时可以保存代码段不变,通过改变数据段中的内容,实现地址无关代码。

Non-PIE

执行程序会在固定的地址开始加载。系统的动态链接器库 ld.so 会首先加载,接着 ld.so 会通过 .dynamic 段中类型为 DT_NEED 的字段查找其他需要加载的共享库,并依次将它们加载到内存。

注意:因为是 Non-PIE 模式,这些动态链接库每次加载的顺序和位置都一样。

PIE

而对于通过 PIE 方式生成的执行程序,因为没有绝对地址引用,所以每次加载的地址都不尽相同。

不仅动态链接库的加载地址不固定,就连执行程序每次加载的地址也不一样。

这就要求 ld.so 首先被加载,然后它不仅要负责重定为其它共享库,还要对可执行文件重定位。

共享动态库原理

多个进程间共享动态链接库的原理

多个进程都链接同一个so动态库,代码段共享,数据段不共享

参考

为项目应用设置No-PIE

位置有关码和位置无关码详细解释

尝试

进程运行都是在各自的虚拟内存,尝试两个进程通过某种手段操作物理内存,从而达到进程间通信的效果。