天天看點

[ Linux ] 程序位址空間

程序位址空間這個名詞可能對于大家來說略顯陌生,但是程式位址空間對于學習過C語言的人來說就不陌生。是以,我們首先複習一下程式位址空間。

1.程式位址空間

1.1 空間布局圖

相比大家在學習C語言的時候都見過這份圖,但是我們對于這個圖并不熟悉

[ Linux ] 程式位址空間

首先請問大家,程式位址空間是記憶體嗎?

其實程式位址空間其實叫做程序位址空間,而程序是作業系統上的概念。是以,程序位址空間分為如上圖所示的幾個部分:

  1. 棧區(stack): 在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。 棧區主要存放運作函數而配置設定的局部變量、函數參數、傳回資料、傳回位址等。
  2. 堆區(heap): 一般由程式員配置設定釋放, 若程式員不釋放,程式結束時可能由 OS 回收 。配置設定方式類似于連結清單。
  3. 資料段(靜态區) ( static ):存放全局變量、靜态資料。程式結束後由系統釋放。
  4. 代碼段: 存放函數體(類成員函數和全局函數)的二進制代碼。

1.2 位址空間驗證

為了更好的了解位址空間分布,我們用代碼來感受以下:

#include <stdio.h> 
#include <stdlib.h>
int un_g_val;//未初始化全局變量
int g_val  =100; //初始化全局變量
int main(int argc,char *argv[],char* env[])
{
  printf("code addr               : %p\n",main);//函數
  printf("init global addr        : %p\n",&g_val);//初始化全局變量
  printf("uninit global addr      : %p\n",&un_g_val);//未初始化全局變量
  char *m1 = (char*)malloc(100);
  char *m2 = (char*)malloc(100);
  char *m3 = (char*)malloc(100);
  char *m4 = (char*)malloc(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("s static addr            : %p\n",&s);//靜态變量
  for(int i  =0;i<argc;i++)
  {
    printf("argv addr              : %p\n",argv[i]);
  }

  for(int i = 0;env[i];i++)
  {
    printf("env addr               : %p\n",env[i]);
  }
  return 0;
}      
[ Linux ] 程式位址空間

其中從下向上,位址由低到高。我們也可以通過程式執行結果檢視這一規律,其中我們能夠發現棧區和堆區中有一個巨大的镂空。

驗證堆棧的增長方向問題:

[ Linux ] 程式位址空間

通過代碼結果我們驗證了堆區向位址增大方向增長,棧區向位址減少方向增長。堆棧相對而生。是以,我們一般在C函數中定義的變量, 通常在棧上儲存,那麼先定義的一定是位址比較高的!

了解static變量

[ Linux ] 程式位址空間

我們能夠發現 變量一旦被static修飾之後,本質是編譯器會把該變量編譯進全局資料區!

2.感覺位址空間的存在

我們仍然使用一段代碼來感覺位址空間

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

int g_val = 100;
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    while(1)
    {
      printf("我是子程序:%d,ppid:%d,g_val:%d,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val);
      sleep(1);
    }
  }
  else
  {
    //parent 
    while(1)
    { 
      printf("我是父程序:%d,ppid:%d,g_val:%d,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val);
      sleep(1); 
    }
  }
  return 0;
}      
[ Linux ] 程式位址空間

這段代碼中,當父子程序都沒有修改全局資料時,父子程序共享該資料,我們看着也沒有什麼問題。而當我們在p子程序中的g_val的值修改成200,會發生什麼事情

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

int g_val = 100;
int main()
{
  pid_t id = fork();
  int flag = 0; 
  if(id == 0)
  {
    //child
    while(1)
    {
      printf("我是子程序:%d,ppid:%d,g_val:%d,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val);
      sleep(1);
      flag++;
      if(flag == 5)
      {
        g_val = 200;
        printf("我是子程序,全局資料我已經修改了,使用者請注意檢視\n");
      }
    }
  }
  else
  {
    //parent 
    while(1)
    {
      printf("我是父程序:%d,ppid:%d,g_val:%d,&g_val:%p\n\n",getpid(),getppid(),g_val,&g_val);
      sleep(1); 
    }
  }
  return 0;
}      
[ Linux ] 程式位址空間

通過列印結果我們驚奇的發現,子程序讀取的g_val是200,父程序讀取的g_val是100,但是他居然是同一塊位址空間,這與我們之前所學習的相違背。是以我們可以得出,我們在C/C++中使用的位址,絕對不是實體位址!!

(因為如果是實體位址,這種現象是不可能産生的)。那如果不是實體位址,那是什麼呢?這種位址叫做虛拟位址。

為什麼作業系統不讓我直接看到實體記憶體呢?

其實記憶體是一個硬體,不能阻攔你通路!隻能被動的進行讀取和寫入!是以如果我們能夠直接通路甚至修改實體記憶體,将會造成不可預料的後果,是以為了安全起見,作業系統不會讓我們直接通路實體記憶體

3.程序位址空間

3.1 概念

概念:每一個程序在啟動的時候,都會讓作業系統給他建立一個位址空間,該位址空間就是程序位址空間。

每一個程序都會有一個自己的程序位址空間!!! 那麼作業系統要不要管理這些程序位址空間呢?雖然我們現在不知道程序位址空間是什麼東西,但是我們知道一定是一個資料結構,作業系統肯定會先描述在組織這些資料結構。而程序位址空間是核心的一個mm_struct。

3.2 了解程序位址空間

是以我們驗證說程式位址空間是不準确的,準确的應該說成程序位址空間,那麼我們應該怎麼來了解呢?

程序位址空間在邏輯上是一個抽象的概念,我們在談這個概念之前我們需要引入一個程序獨立性。

  • 程序獨立性:程序獨立性是指多程序運作,需要獨享各種資源,多程序運作期間互不幹擾。

程序位址空間存在的意義是讓每一個程序都認為自己是獨占系統中的所有資源的!!所謂的位址空間其實就是OS通過軟體的方式,給程序提供一個軟體視角,認為自己會獨占系統的所有資源(記憶體)。請看下圖:

[ Linux ] 程式位址空間

上面的圖就足矣說名問題,同一個變量,位址相同,其實是虛拟位址相同,内容不同其實是被映射到了

不同的實體位址!

區域

在mm_struct中,我們知道有不同的區域,棧區、堆區、全局資料區等等,在核心中是以下面這段代碼所示存儲的

struct mm_struct
{
  long code_start;
  long code_end;

  long init_start;
  long init_end;

  long uninit_start;
  long uninit_end;

  long heap_start;
  long heap_end;

  long stack_start;
  long stack_end;
  ...
}      

是以位址空間就可以被劃分為不同的區域,每一個區域範圍之内都可以有一套位址作為頁表中的虛拟位址和實體位址進行映射。頁表映射是将程式加在到記憶體有内程式變成程序之後,由OS給每個程序建構一個頁表結構。

檢視核心源碼

[ Linux ] 程式位址空間

3.3 程式是如何變成程序的

程式被編譯出來,沒有被加載的時候,程式内部有位址嗎?

答:有的。連結過程是把已有程式和庫當中的代碼産生關聯,沒有位址怎麼進行調用呢?

程式被編譯出來,沒有被加載的時候,程式内部有區域嗎?

答:有的。我們可以在Linux下使用指令進行檢視可執行程式,可以驗證每一個可執行程式是有位址的,一個程式沒有被加載的時候是有位址的。

readelf -S test      
[ Linux ] 程式位址空間

當父程序或者子程序在寫入的時候,由于程序具有獨立性,如果子程序将同一個變量修改時,作業系統會重新給子程序重新開辟一段空間,建立映射關系,是以,最終我們檢視時,雖然位址一樣,但是所指的資料已經不同了。這種我們也叫做寫實拷貝。

[ Linux ] 程式位址空間

fork有兩個傳回值,同一個變量怎麼會有兩個傳回值

在之前,我們知道fork有兩個傳回值,而同一個變量怎麼會有兩個傳回值,這時我們就可以了解pid_t id是屬于父程序棧空間中定義的變量,fork内部,return會被執行兩次,return的本質,就是通過寄存器将傳回值寫入到結構傳回值的變量中!!當id=fork()的時候,誰先傳回,誰就要發生寫實拷貝,是以,同一個變量會有不同的内容值,本質是因為大家的虛拟位址是一樣的,但是大家對應的實體位址是不一樣的!

為什麼要有虛拟位址空間?

安全性:

在前面說過如果隻有實體記憶體,沒有虛拟位址時,直接讓程序通路實體記憶體是不安全的。那為什麼有虛拟位址就是安全的呢?因為通過頁表轉換,如果是一個不安全或者非法的指針,通路實體記憶體時,通過頁表進行轉換過程進行稽核,非法的通路就可以直接攔截了。

程序管理:

用多少開多少,什麼時候用是麼時候開!通過位址空間,進行功能子產品的解耦!!

讓程序或者程式可以以一種統一的視角看待記憶體: