『 Linux 』 进程地址空间与动态库地址

文章目录


逻辑地址

在程序加载进内存之前,即编译之后就已经形成了地址,在编译之后的地址被称为 逻辑地址;

  • 逻辑地址

    逻辑地址是程序在编译时产生的地址;

    在编译阶段,编译器会为程序中的每个变量函数等元素生成一个相对的地址即逻辑地址;

    这些地址是相对于程序开始的位置而言,他们不是物理内存地址;

假设一个源文件(main.c)中内容为:

c 复制代码
#include <stdio.h>

int main()
{
  printf("hello world\n");
  int x = 10;
  int y = 20;
  int z = x + y;
  printf("z = x + y = %d\n", z);
  return 0;
}

使用gcc将源文件进行编译链接形成可执行程序a.out;

bash 复制代码
gcc main.c

利用objdump工具选择-S选项可以观察可执行程序的反汇编情况,在此处即objdump -S a.out;

bash 复制代码
$ objdump -S a.out 

a.out:     file format elf64-x86-64


Disassembly of section .init:

0000000000400418 <_init>:
  400418:   48 83 ec 08             sub    $0x8,%rsp
  40041c:   48 8b 05 d5 0b 20 00    mov    0x200bd5(%rip),%rax        # 600ff8 <__gmon_start__>
  400423:   48 85 c0                test   %rax,%rax
  400426:   74 05                   je     40042d <_init+0x15>
  400428:   e8 53 00 00 00          callq  400480 <__gmon_start__@plt>
  40042d:   48 83 c4 08             add    $0x8,%rsp
  400431:   c3                      retq   

# ......
# 略....
# ......


0000000000400620 <__libc_csu_fini>:
  400620:   f3 c3                   repz retq 

Disassembly of section .fini:

0000000000400624 <_fini>:
  400624:   48 83 ec 08             sub    $0x8,%rsp
  400628:   48 83 c4 08             add    $0x8,%rsp
  40062c:   c3                      retq   

其中代码中40041840062c即为编译后所生成的逻辑地址;

源文件进行编译后生成可执行程序后将存在逻辑地址;

逻辑地址一般按照段进行分布,意思就是当编译过后最终的指令地址将以不同的段落进行分组;

每个分组被称为段描述,主要的段包括:

  • .text

    包含编译后的机器指令,即程序代码;

  • .data

    包含已初始化的全局变量和静态变量;

  • .rodata\.rdonly

    包含只读数据,例如字符串常量等;

  • .bss

    用于未初始化的全局变量和静态变量;

    一般情况下该段不占用实际的文件空间,但是会被标记在程序运行时需要分配的空间;

  • .symtab段(符号表),.strtab段(字符串表),.debug

    包含用于调试和符号解析的信息;

在使用objdump -S时主要为用户展示主要为.text段与源代码(如果可用)中的内容;

若是需要查看整体的逻辑地址段描述的细节可以使用-h选项:

bash 复制代码
$ objdump -h a.out 

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  0000000000400254  0000000000400254  00000254  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  0000000000400274  0000000000400274  00000274  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     0000001c  0000000000400298  0000000000400298  00000298  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 #...
 
 19 .dynamic      000001d0  0000000000600e28  0000000000600e28  00000e28  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .got          00000008  0000000000600ff8  0000000000600ff8  00000ff8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got.plt      00000038  0000000000601000  0000000000601000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .data         00000004  0000000000601038  0000000000601038  00001038  2**0
                  CONTENTS, ALLOC, LOAD, DATA
 23 .bss          00000004  000000000060103c  000000000060103c  0000103c  2**0
                  ALLOC
 24 .comment      00000059  0000000000000000  0000000000000000  0000103c  2**0
                  CONTENTS, READONLY

由于编译器并不知道程序将被加载到内存的具体的哪个位置,因此实际上是一种相对地址;

这些地址基于程序的起始点或某个特定段的起始点;


进程地址空间

程序在加载进内存后会成为进程,操作系统会为进程维护一个进程地址空间(虚拟内存)从而保持物理内存的安全性;

进程地址空间一般是由逻辑地址直接加载进内存的;

当逻辑地址被加载进内存时将会进行一个至进程地址空间的一个转换;

最终通过内存管理单元MMU与页表的映射机制映射至物理内存;


指令的执行

被编译生成的可执行程序在执行时将会加载进内存;

实际上加载进内存的即为可执行程序的代码和数据;

代码即为编译生成的各个指令集以及其虚拟地址的映射;

所以实际上指令集中的每个指令都将存在其对应的物理地址;

CPU中存在一个寄存器为EIPpc指针(指令寄存器);

当可执行程序被加载进内存成为进程时,操作系统将会给这个寄存器中存放这个进程代码的初始位置(虚拟地址 ),例如程序的入口点;

CPU将会根据EIP中指向的地址从进程的虚拟地址空间通过页表映射读取指令,并让EIP读取下一条指令的地址;

实际上EIP所读取到的指令地址都是虚拟地址;

在整个内存管理系统中,真正代表虚拟地址到物理地址映射的只能通过内存管理单元MMU与页表之间的映射;

以该图为例为以下几点:

  • 加载可执行程序

    可执行程序(如1.exe)从磁盘中加载进物理内存;

    加载过程中虚拟地址被分配给程序的各个部分(如代码段,数据段等);

  • 虚拟地址与物理地址的映射

    程序的虚拟地址包括代码段,已初始化数据段,堆,共享区等;

    页表负责将这些虚拟地址映射到物理地址;

  • 指令执行过程

    当进程被调度执行时,CPU中的EIP(指令寄存器)会指向进程代码的入口地址(虚拟地址);

    EIP寄存器中的地址通过页表映射到物理地址从而读取指令;

    CPU根据EIP指向的地址从进程的虚拟地址空间中读取指令并执行指令;

  • 内存管理单元(MMU)

    MMU在这个过程中起到关键作用,负责将虚拟地址转化为物理地址;

    MMU通过查询页表来完成地之间的转换;


惰性加载(Lazy Loading)/延迟加载

一般情况下一个可执行程序执行成为进程后操作系统将会优先为其维护对应的task_struct,进程地址空间,页表等结构体,但不一定会第一时间将所有的代码数据都加载进物理内存当中;

这是一种按需加载的策略;

当存运行过程中遇到了未被加载进物理内存的代码和数据时内存管理单元MMU将会触发缺页中断异常;

当这个异常被操作系统检测到时将会从磁盘中找到对应的代码数据将其加载进内存当中;

通过这种方式,操作系统可以有效管理有限的物理内存资源从而确保那些确实需要被立即访问的数据代码有限被加载进物理内存而不必一开始就分配所有资源从而提高系统的整体性能和响应速度;


动态库的地址

动态库是一个当一个可执行程序依赖时需要外部链接的库;

与静态库不同,在进行链接时静态库的代码数据将直接以拷贝的形式使得其能够与可执行程序融为一体;

而动态库则是当加载器将其加载到物理内存中就只会独一份并且可以被其他依赖该动态库的进程共享;

所以动态库在加载的过程中是不能确定其具体加载的位置的;

在使用gcc/g++生成动态库时需要使用选项gcc -fPIC,这个选项是生成一个与位置无关码;

  • 位置无关码(PIC)

    这是一种特殊类型的机器码,他可以在内存中的任何位置执行而不需要修改;

    这是通过确保代码执行时不依赖它的绝对地址来实现的,即代码对于数据的禁用是基于它当前的运行地址来计算的;

动态库被设计为可以被多个进程共享;

为了使不同的进程能够同时使用同一份物理内存的库副本,库中的代码必须能够运行在任何地址上即它们必须是与位置无关的;

  • 内存效率

    通过使用位置无关码,操作系统可以更有效的管理内存;

    如,它可以避免为每个使用特定库的进程单独加载库的副本从而节省内存资源;

  • 动态加载

    位置无关码允许动态库在运行时被加载到任意位置使得在链接时提供灵活性;

使用-fPIC选项所生成的.o目标文件最终被生成的动态库其中的地址将可以加载到物理内存中的任意位置,并随机为其分配进程地址空间;

假设存在一个动态库函数,编译为位置无关码:

c 复制代码
// example.c
int add(int a, int b) {
    return a + b;
}

编译这个库为位置无关码:

bash 复制代码
gcc -shared -fPIC -o libexample.so example.c

编译完成后使用objdump查看动态库的反汇编:

bash 复制代码
objdump -d libexample.so

# 简化

00000000000006a0 <add>:
 6a0:   55                      push   %rbp
 6a1:   48 89 e5                mov    %rsp,%rbp
 6a4:   89 7d fc                mov    %edi,-0x4(%rbp)
 6a7:   89 75 f8                mov    %esi,-0x8(%rbp)
 6aa:   8b 55 fc                mov    -0x4(%rbp),%edx
 6ad:   8b 45 f8                mov    -0x8(%rbp),%eax
 6b0:   01 d0                   add    %edx,%eax
 6b2:   5d                      pop    %rbp
 6b3:   c3                      retq

在该例子中显示的为函数add的机器码;

其中使用的是相对寄存器的操作而并没有绝对地址的引用;

动态库中的代码执行时将一句当前的指令指针(EIP/RIP)计算相对地址;

即使代码被加载到内存的不同为止也可以使用相对于当前指令的偏移量来访问数据和其他指令;

相关推荐
C++忠实粉丝几秒前
Linux环境基础开发工具使用(2)
linux·运维·服务器
康熙38bdc24 分钟前
Linux 环境变量
linux·运维·服务器
存储服务专家StorageExpert39 分钟前
DELL SC compellent存储的四种访问方式
运维·服务器·存储维护·emc存储
hakesashou1 小时前
python如何比较字符串
linux·开发语言·python
Ljubim.te1 小时前
Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】
linux·学习·centos
cooldream20092 小时前
Linux性能调优技巧
linux
大G哥2 小时前
记一次K8S 环境应用nginx stable-alpine 解析内部域名失败排查思路
运维·nginx·云原生·容器·kubernetes
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
醉颜凉2 小时前
银河麒麟桌面操作系统修改默认Shell为Bash
运维·服务器·开发语言·bash·kylin·国产化·银河麒麟操作系统
QMCY_jason2 小时前
Ubuntu 安装RUST
linux·ubuntu·rust