天天看點

【Linux】程序位址空間

本篇部落格來認識一下linux下程式位址空間的概念

示範所用系統:CentOS 7.6

文章目錄

  • ​​1.引入程式位址空間​​
  • ​​1.1 驗證不同區域​​
  • ​​1.2 fork感覺位址空間的存在​​
  • ​​2.簡述程式位址空間​​
  • ​​2.1 程式位址空間和代碼編譯​​
  • ​​2.2 寫時拷貝​​
  • ​​fork兩個傳回值的解釋​​
  • ​​3.程式位址空間的作用​​
  • ​​結語​​

1.引入程式位址空間

之前學習​

​C/C++​

​的時候,多少應該都聽過棧區/堆區/靜态區/全局區的概念,還有一張很經典的示範圖,大部分講解這幾個記憶體區域的圖檔都和下圖類似

【Linux】程式位址空間

但是有一個問題,這裡的程式位址空間,是我們的實體記憶體上的東西嗎?

并不是!

  • 程式/程序位址空間是作業系統上的概念,它和我們實體記憶體本身不是一個東西

1.1 驗證不同區域

用下面這個代碼來簡單驗證一下不同區域上的差別

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

int un_global_val;//未初始化全局變量
int global_val=100;//已初始化全局變量
//main函數的參數
int main(int argc, char *argv[], char *env[])
{
    printf("code addr         : %p\n", main);
    printf("init global addr  : %p\n", &global_val);
    printf("uninit global addr: %p\n", &un_global_val);
    char *m1 = (char*)malloc(100);
    char *m2 = (char*)malloc(100);
    char *m3 = (char*)malloc(100);
    char *m4 = (char*)malloc(100);
    int a = 100;
    static int s = 100;
    printf("heap addr         : %p\n", m1);
    printf("heap addr         : %p\n", m2);
    printf("heap addr         : %p\n", m3);
    printf("heap addr         : %p\n", m4);

    printf("stack addr        : %p\n", &m1);
    printf("stack addr        : %p\n", &m2);
    printf("stack addr        : %p\n", &m3);
    printf("stack addr        : %p\n", &m4);
    printf("stack addr a      : %p\n", &a);
    printf("stack addr s      : %p\n", &s);
    printf("\n");
    for(int i = 0; i < argc; i++)
    {
        printf("argv addr         : %p\n", argv[i]);
    }
    printf("\n");
    for(int i =0 ; env[i];i++)
    {
        printf("env addr          : %p\n", env[i]);
    }
    return 0;
}      
【Linux】程式位址空間

通過上面的測試,可以看到其結果和文章最開始的那張圖相同。這裡解釋一下​

​向上/向下​

​的含義

  • 向上增長:向位址增大的方向增長
  • 向下增長:向位址減小的方向增長

不過那個圖檔内部還少了一些東西,比如指令行參數和環境變量其實是存放在棧區之上的。補全之後的圖檔如下

【Linux】程式位址空間

其中我們還可以發現,棧區和堆區之間有非常大的記憶體空隙

heap addr         : 0x1a140f0
heap addr         : 0x1a14160
stack addr        : 0x7ffe6671ec60
stack addr        : 0x7ffe6671ec58      

因為在C/C++中定義的變量都是在棧上儲存的,棧向下增長,先定義的變量位址較高!

int a = 100;
    static int s = 100;      

關于函數中​

​static​

​​修飾的變量,可以看到其位址空間屬于全局靜态區。雖然在函數中用​

​static​

​修飾是限制其隻能在該函數内通路,但是該變量的聲明周期是跟随整個程式的!

stack addr a      : 0x7ffe6671ec44
stack addr s      : 0x601048      

說了這麼多,我們也沒看看出來程式位址空間在哪兒啊?

1.2 fork感覺位址空間的存在

下面可以用一個簡單的fork代碼來确認程式位址空間的存在!

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>

int main()
{
    int test = 10;
    int ret = fork();
    if(ret == 0)
    {
        while(1)
        {
            printf("我是子程序%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
            sleep(1);
        }
    }
    else
    {    
        while(1)
        {
            printf("我是父程序%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
            sleep(1);
        }
    }       
    return 0;
}      

依舊是最簡單的一個fork代碼,正常情況下,二者列印的結果應該是一樣的!

【Linux】程式位址空間

可如果我們在子程序中修改一下test呢?

【Linux】程式位址空間

這時候就會發現一個離譜的現象:子程序和父程序列印的test值不一樣,但是其位址卻完全相同!

如果我們在​

​C/C++​

​中使用的位址就是實體位址,是不可能出現這種情況的!怎麼可能在實體記憶體的同一個位址通路出兩個不同的結果呢?

就好比張三和李四在同一天的同一時間去了​

​AA路30号​

​這個位址,不可能會出現張三去了發現是超市,而李四去了發現是醫院的情況

這便告訴我們了程式位址空間的存在,亦或者說,我們在程式設計中使用的位址都是虛拟位址

2.簡述程式位址空間

每一個程序在啟動的時候,都會讓作業系統給其配置設定一個位址空間,這就是程序位址空間

  • 以​

    ​先描述再組織​

    ​​的理念,程序位址空間其實是作業系統核心的一個資料結構​

    ​struct mm_struct​

  • 之前提到過程序具有獨立性,在多程序運作的時候,需要獨享各種資源。而程序位址空間的作用,就是讓程序認為自己是獨占作業系統中的所有資源!

這個操作,其實就是作業系統給該進程序畫了一個假的記憶體(虛拟位址)程序需要記憶體的時候,作業系統就會在頁表裡面畫一個位址給他,再将該位址映射到實體記憶體上面

【Linux】程式位址空間

在Linux源碼中可以看到這玩意的存在,其中的​

​struct vm_area_struct * mmap;​

​就是一個我們的頁表

【Linux】程式位址空間

這裡就能看到虛拟位址空間的start和end了!

【Linux】程式位址空間

2.1 程式位址空間和代碼編譯

我們直到,C語言代碼需要經過​

​預處理-編譯-連結-彙編​

​這幾個步驟

  • 程式編譯出來,沒有被加載的時候,程式内部有位址(如果沒有位址,無法進行連結)
  • 程式編譯出來,沒有被加載的時候,程式内部有區域(​

    ​readelf -s 可執行檔案​

    ​可以檢視區域)
[muxue@bt-7274:~/git/raspi/code/22-10-07_程式位址空間]$ readelf -S test
There are 30 section headers, starting at offset 0x19f8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       00000000000000c0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400378  00000378
       0000000000000059  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           00000000004003d2  000003d2
       0000000000000010  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          00000000004003e8  000003e8
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400408  00000408
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400420  00000420
       00000000000000a8  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         00000000004004c8  000004c8
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004004f0  000004f0
       0000000000000080  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400570  00000570
       00000000000001e2  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000400754  00000754
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000400760  00000760
       000000000000005e  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         00000000004007c0  000007c0
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         00000000004007f8  000007f8
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000050  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000601050  00001050
       0000000000000004  0000000000000000  WA       0     0     1
  [25] .bss              NOBITS           0000000000601054  00001054
       0000000000000004  0000000000000000  WA       0     0     1
  [26] .comment          PROGBITS         0000000000000000  00001054
       000000000000002d  0000000000000001  MS       0     0     1
  [27] .symtab           SYMTAB           0000000000000000  00001088
       0000000000000648  0000000000000018          28    46     8
  [28] .strtab           STRTAB           0000000000000000  000016d0
       000000000000021e  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  000018ee
       0000000000000108  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)      

需要注意的是,程式内部的位址,和記憶體的位址沒有關系

可以了解為,我們程式内部都存放的是一個相對位址。編譯程式的時候,認為程式是按照​

​0000~FFFF​

​進行編址的。

當程式被加載到記憶體當中時,假設系統将該程式的代碼從記憶體​

​0x100​

​開始加載,就可以依照程式編址的資料加上這個偏移量,進而存放在記憶體中。

比如程式中有一個代碼段的位置是​

​0x1F​

​,這時候在加載程式的時候,就會把這個代碼段加上偏移量來加載

代碼位址 虛拟位址
0x1f 0x11f
0x20 0x120

大概就是這樣,吧哩吧啦……

2.2 寫時拷貝

現在就可以來解答一下​

​1.2​

​中出現的問題了

【Linux】程式位址空間

當子程序嘗試修改test變量的時候,作業系統就會開始一個寫時拷貝,開辟一個新的空間,将對應的值考入該空間,再重新映射頁表。

這時候,雖然頁表左側的虛拟位址沒有變化,但是映射的實體位址已經不一樣了!

【Linux】程式位址空間

這樣就能保證父子程序的獨立性,誰修改變量都互不影響!

類似C++中實作的深拷貝!

fork兩個傳回值的解釋

​pid_t id​

​這個變量屬于父程序棧空間中定義的變量,但是fork内部,return會被執行兩次(return的本質是通過寄存器将傳回值寫入到接收傳回值的變量中)

當​

​id = fork()​

​的時候,誰先傳回,誰就會發生一次寫時拷貝。是以同一個變量有不同的内容值,本質上也是同一個虛拟位址,對應了不同實體位址的展現!

  • 列印​

    ​fork​

    ​​的傳回值,即可觀察到和​

    ​1.2​

    ​中一樣的情況,虛拟位址相同,但是ret的值不同
【Linux】程式位址空間

3.程式位址空間的作用

需要注意的是,記憶體作為一個硬體,沒有辦法拒絕你的讀寫!記憶體是不帶控制功能的!

直接讓使用者修改實體記憶體風險極大:

  • 野指針問題
  • 使用者可能直接修改作業系統需要用到的記憶體位址,導緻系統boom

程式位址空間讓通路記憶體時添加了一層軟硬體層,可以對轉化過程進行稽核,攔截非法的通路

  • 保護記憶體
  • 可以使用程序管理更好的對功能子產品進行解耦(linux記憶體管理)
  • 讓程式/程序可以用統一的方式/視角來看待記憶體,以統一的方式編譯加載所有可執行程式,簡化程式本身的設計和實作

同時,程式位址空間還可以延遲使用者的記憶體使用。比如我們現在​

​malloc​

​​了100個位元組的空間,實際上作業系統并不會立馬給你申請空間,而是操作你的​

​mm_struct​

​讓程序以為自己已經申請成功了。當程式真正使用這個空間的時候,作業系統才會去實體記憶體中進行映射!

申請的時候,是通過linux的記憶體管理子產品進行操作的。該子產品隻負責開辟記憶體,而不管開辟記憶體的用途

結語