物理地址,虚拟地址,相对地址和绝对地址

之前在看linux编译链接、重定向的原理的时候,总会看到物理地址、虚拟地址、相对地址和绝对地址这些名词,结合上下文自己还是不理解这几个名次的含义。最近在研究看不定参数函数的参数解析的原理对这几个名词有进一步理解。

1 物理地址

在早期的操作系统中,没有虚拟内存管理,物理地址就是真实的内存地址,应用程序直接跑在物理地址上。应用程序也可以随意访问、操作任意的物理地址。这样虽然应用程序加载方便,但是弊大于利,随意操作物理地址导致重要的寄存器或者重要的数据被修改。对于应用程序和操作系统来讲是不安全的。

在嵌入式开发过程中,或者是在开发BIOS的过程中,如果是没有操作系统或者是没有启用MMU的时候,就是直接操作物理地址的。例如,在芯片的boot阶段,需要去初始化芯片或者是驱动外设(例如i2c、espi等),查看芯片的data sheet芯片的寄存器的地址和寄存器的功能,然后直接读写寄存器就可以完成芯片的初始化或者驱动外设等功能。

例如2600芯片上定义0x1e6ee000是2600的espi控制器的寄存器起始地址。当我们根据eSPI协议驱动控制器时,就是直接写的物理地址上寄存器。如果是使用MMU,那么又应该如何使用呢?

2 相对地址

现代操作系统支持虚拟内存管理。进程与进程之间内存隔离,每个进程都有属于自己的地址空间。那么其他进程就无法随意操作本进程的地址空间,保证了程序的正常运行。在两个终端上运行相同的应用程序,这两个应用程序运行加载的虚拟地址是相同的,但是实际对应的物理地址是不一样的。编写最简单的hello world程序:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    printf("hello world!\n");
    return 0;

编译生成可执行文件:

gcc -g -o main main.c

通过这个例子一并把相对位置的概念弄清楚。先通过反汇编查看下汇编指令对应的地址:

objdump -S main
0000000000001149 <main>:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    1149:       f3 0f 1e fa             endbr64 
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    printf("hello world!\n");
    1151:       48 8d 3d ac 0e 00 00    lea    0xeac(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1158:       e8 f3 fe ff ff          callq  1050 <puts@plt>
    return 0;
    115d:       b8 00 00 00 00          mov    $0x0,%eax
    1162:       5d                      pop    %rbp
    1163:       c3                      retq   
    1164:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
    116b:       00 00 00 
    116e:       66 90                   xchg   %ax,%ax
}

可以看到main函数的在0x1149处,也就是说编译过程中链接器给main函数指定的加载地址是0x1149。紧接着,用gdb进行调试,查看实际运行地址是多少:

gdb-multiarch ./main 

在main函数设下断点,然后运行程序:

GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.2) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.
--Type <RET> for more, q to quit, c to continue without paging--

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./main...
(gdb) b main
Breakpoint 1 at 0x1149: file main.c, line 6.
(gdb) r
Starting program: /home/xucanming/code/Test/lua_webserver/main 

Breakpoint 1, main () at main.c:6
6       {

在gdb中查看进程的地址映射:

(gdb) info proc mappings
process 3107772
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x555555554000     0x555555555000     0x1000        0x0 /home/user/code/Test/lua_webserver/main
      0x555555555000     0x555555556000     0x1000     0x1000 /home/user/code/Test/lua_webserver/main
      0x555555556000     0x555555557000     0x1000     0x2000 /home/user/code/Test/lua_webserver/main
      0x555555557000     0x555555558000     0x1000     0x2000 /home/user/code/Test/lua_webserver/main
      0x555555558000     0x555555559000     0x1000     0x3000 /home/user/code/Test/lua_webserver/main

可以看到main进程被加载到内存基地址为0x555555554000内存上。那么实际上被加载到哪里了呢?前面我们给main函数设置了断点,并运行程序,运行到main函数处,触发了断点,此时只需要把pc指针的值打印出来即可:

(gdb) disassemble main
Dump of assembler code for function main:
=> 0x0000555555555149 <+0>:     endbr64 
   0x000055555555514d <+4>:     push   %rbp
   0x000055555555514e <+5>:     mov    %rsp,%rbp
   0x0000555555555151 <+8>:     lea    0xeac(%rip),%rdi        # 0x555555556004
   0x0000555555555158 <+15>:    callq  0x555555555050 <puts@plt>
   0x000055555555515d <+20>:    mov    $0x0,%eax
   0x0000555555555162 <+25>:    pop    %rbp
   0x0000555555555163 <+26>:    retq   
End of assembler dump.
(gdb) p /x $pc
$1 = 0x555555555149

查看反汇编代码和pc指针,两者的值是相同的。main进程实际的加载地址是:0x0000555555555149。显然,实际的加载地址是编译时的链接地址加上一个操作系统给的基地址。为什么说是操作系统给的呢?因为实际上(以使用的linux系统为例)操作系统默认是开启地址空间分布随机化(ALSR)的,目的就是为了防止被攻击。一般这个基地址是:0x555555554000那么编译时链接器给的地址就是一个相对地址不管操作系统把程序加载到哪个地址上,只要使用MMU开启地址映射,这些地址都是虚拟地址

3 绝对地址

动态库在编译时都会都会使用地址无关代码(即PIC),因为编译出来的代码的链接地址可能和实际运行地址不一样。所以就会用地址无关的代码,在函数寻址时,只需要基地址加上编译生成的链接地址,就可以找到对应的函数地址。同样的,应用程序可以编译和链接成地址无关的可执行程序(PIE)。

-fno-pie:编译选项,要求生成非位置无关的代码(使用绝对地址)。
-no-pie:链接选项,要求链接器生成非位置无关的可执行文件(传统可执行文件类型)。

一般在编译非位置无关的可执行程序时,上面两个编译选项需要配合使用。

gcc -fno-pie  -no-pie -g -o main main.c

同样的,通过GDB调试查看程序的运行地址:

(gdb) b main
Breakpoint 1 at 0x401136: file main.c, line 6.
(gdb) r
Starting program: /home/xucanming/code/Test/lua_webserver/main 

Breakpoint 1, main () at main.c:6
6       {
(gdb) disassemble main
Dump of assembler code for function main:
=> 0x0000000000401136 <+0>:     endbr64 
   0x000000000040113a <+4>:     push   %rbp
   0x000000000040113b <+5>:     mov    %rsp,%rbp
   0x000000000040113e <+8>:     mov    $0x402004,%edi
   0x0000000000401143 <+13>:    callq  0x401040 <puts@plt>
   0x0000000000401148 <+18>:    mov    $0x0,%eax
   0x000000000040114d <+23>:    pop    %rbp
   0x000000000040114e <+24>:    retq   
End of assembler dump.
(gdb) 

可以看到main被加载到0x0000000000401136上执行。再通过objdump -S main查看编译链接地址:

0000000000401136 <main>:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
  401136:       f3 0f 1e fa             endbr64 
  40113a:       55                      push   %rbp
  40113b:       48 89 e5                mov    %rsp,%rbp
    printf("hello world!\n");
  40113e:       bf 04 20 40 00          mov    $0x402004,%edi
  401143:       e8 f8 fe ff ff          callq  401040 <puts@plt>
    return 0;
  401148:       b8 00 00 00 00          mov    $0x0,%eax
  40114d:       5d                      pop    %rbp
  40114e:       c3                      retq   
  40114f:       90                      nop

实际上链接地址和加载运行地址相同这个时候函数寻址、读取内存等操作的都是绝对地址绝对地址是个相对概念,与是否是物理地址还是虚拟地址无关。PIE的加载地址每个操作系统都不一样:

架构 默认基地址 范围 特点
x86 0x08048000 0x08000000-0xbfffffff 固定地址
x86_64 0x400000 0x400000-0x7fffffffffff 高半区
ARM32 0x8000 0x00010000-0xbfffffff 对齐要求
ARM64 0x400000 0x400000-0x7fffffffff 大地址空间
posted @ 2025-07-16 17:46  cockpunctual  阅读(255)  评论(0)    收藏  举报