'從編寫源代碼到程序在內存中運行的全過程解析'

GCC 彙編語言 Linux 操作系統 餘生做酒長醉不憂 2019-08-18
"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

從編寫源代碼到程序在內存中運行的全過程解析

(5)符號重定位

因為在編譯過程中不分配地址,所以在目標文件所以數據出現的地方都給的是零地址,所有函數調用的地方給的是相對於下一條指令的地址的偏移量。在符號重定位時,要把分配的地址回填到數據和函數調用出現的地方,而且對於數據而言填的是絕對地址,而對函數調用而言填的是偏移量

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

從編寫源代碼到程序在內存中運行的全過程解析

(5)符號重定位

因為在編譯過程中不分配地址,所以在目標文件所以數據出現的地方都給的是零地址,所有函數調用的地方給的是相對於下一條指令的地址的偏移量。在符號重定位時,要把分配的地址回填到數據和函數調用出現的地方,而且對於數據而言填的是絕對地址,而對函數調用而言填的是偏移量

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

從編寫源代碼到程序在內存中運行的全過程解析

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

四、可執行程序

鏈接完成以後形成了可執行文件,下面來解析可執行文件是如何執行起來的。同樣,首先給出可執行文件的總體佈局,然後再來深入解析。

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

從編寫源代碼到程序在內存中運行的全過程解析

(5)符號重定位

因為在編譯過程中不分配地址,所以在目標文件所以數據出現的地方都給的是零地址,所有函數調用的地方給的是相對於下一條指令的地址的偏移量。在符號重定位時,要把分配的地址回填到數據和函數調用出現的地方,而且對於數據而言填的是絕對地址,而對函數調用而言填的是偏移量

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

從編寫源代碼到程序在內存中運行的全過程解析

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

四、可執行程序

鏈接完成以後形成了可執行文件,下面來解析可執行文件是如何執行起來的。同樣,首先給出可執行文件的總體佈局,然後再來深入解析。

從編寫源代碼到程序在內存中運行的全過程解析

首先看一下可執行文件的頭部,如下圖,裡面記錄了函數的入口點地址為0x08048094(後面會解釋這個值的來由),還有就是size of this headers,程序頭部佔52個字節,然後還有三個program headers,每個program headers佔32字節,共佔3*32=96字節,所以程序頭部+program heades=52+96=0x94,而從虛擬地址空間佈局可知.text段正好是從0x08048000開始的,所以可執行程序的入口點就是0x08048000+0x94=0x08048094:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

從編寫源代碼到程序在內存中運行的全過程解析

(5)符號重定位

因為在編譯過程中不分配地址,所以在目標文件所以數據出現的地方都給的是零地址,所有函數調用的地方給的是相對於下一條指令的地址的偏移量。在符號重定位時,要把分配的地址回填到數據和函數調用出現的地方,而且對於數據而言填的是絕對地址,而對函數調用而言填的是偏移量

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

從編寫源代碼到程序在內存中運行的全過程解析

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

四、可執行程序

鏈接完成以後形成了可執行文件,下面來解析可執行文件是如何執行起來的。同樣,首先給出可執行文件的總體佈局,然後再來深入解析。

從編寫源代碼到程序在內存中運行的全過程解析

首先看一下可執行文件的頭部,如下圖,裡面記錄了函數的入口點地址為0x08048094(後面會解釋這個值的來由),還有就是size of this headers,程序頭部佔52個字節,然後還有三個program headers,每個program headers佔32字節,共佔3*32=96字節,所以程序頭部+program heades=52+96=0x94,而從虛擬地址空間佈局可知.text段正好是從0x08048000開始的,所以可執行程序的入口點就是0x08048000+0x94=0x08048094:

從編寫源代碼到程序在內存中運行的全過程解析

然後看看這三個program headers裡面的內容,第一個load項的屬性是可讀可執行,其實存放的就是代碼段;第二個load項的屬性是可讀可寫,其實存放的就是數據段。這兩個load項的意義在於它指示了哪些段會被加載到同一個頁面中:

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

從編寫源代碼到程序在內存中運行的全過程解析

(5)符號重定位

因為在編譯過程中不分配地址,所以在目標文件所以數據出現的地方都給的是零地址,所有函數調用的地方給的是相對於下一條指令的地址的偏移量。在符號重定位時,要把分配的地址回填到數據和函數調用出現的地方,而且對於數據而言填的是絕對地址,而對函數調用而言填的是偏移量

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

從編寫源代碼到程序在內存中運行的全過程解析

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

四、可執行程序

鏈接完成以後形成了可執行文件,下面來解析可執行文件是如何執行起來的。同樣,首先給出可執行文件的總體佈局,然後再來深入解析。

從編寫源代碼到程序在內存中運行的全過程解析

首先看一下可執行文件的頭部,如下圖,裡面記錄了函數的入口點地址為0x08048094(後面會解釋這個值的來由),還有就是size of this headers,程序頭部佔52個字節,然後還有三個program headers,每個program headers佔32字節,共佔3*32=96字節,所以程序頭部+program heades=52+96=0x94,而從虛擬地址空間佈局可知.text段正好是從0x08048000開始的,所以可執行程序的入口點就是0x08048000+0x94=0x08048094:

從編寫源代碼到程序在內存中運行的全過程解析

然後看看這三個program headers裡面的內容,第一個load項的屬性是可讀可執行,其實存放的就是代碼段;第二個load項的屬性是可讀可寫,其實存放的就是數據段。這兩個load項的意義在於它指示了哪些段會被加載到同一個頁面中:

從編寫源代碼到程序在內存中運行的全過程解析

可以看到這兩個load項的對齊方式是頁面對齊(32位linux操作系統頁面大小為4K)。

當雙擊一個可執行程序時,首先解析其文件頭部ELF header獲取entry point address程序入口點地址,然後按照兩個load項的指示將相應的段通過mmap()函數映射到虛擬頁面中(虛擬頁面存在於虛擬地址空間中),最後再通過多級頁表映射將虛擬頁面映射到物理頁面

"


int gdata1 = 10;
int gdata2 = 0;
int gdata3;

static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;

int main(void)
{
\tint a = 12;
\tint b = 0;
\tint c;

\tstatic int d = 13;
\tstatic int e = 0;
\tstatic int f;
\treturn 0;
}

一、基本概念

1.什麼是數據

大家平時口中經常說程序是由程序代碼、數據和進程控制塊組成,但是很多人卻不知道什麼是數據。這裡我們搞清楚兩件事情,一是什麼是數據,二是數據存放在哪裡。

(1)數據

數據指的是稱序中定義的全局變量和靜態變量。還有一種特殊的數據叫做常量。所以上面的的gdata1、gdata2、gdata3、gdata4、gdata5、gdata6、d、e和f均是數據。

(2)數據存放在哪裡

數據存放的區域有三個地方:.data段、.bss段和.rodata段。那麼你肯定想知道數據是如何放在這三個段中的,怎麼區分

對於初始化不為0的全局變量和靜態變量存放在.data段,即gdata1、gdata4和d存放在.data段;對於未初始化或者初始化值為0的段存放在.bss段中,而且不佔目標文件的空間,即gdata2、gdata3、gdata5、gdata6、e和f存放在.bss段。文章下面有一張關於符號表的圖,大家可以看到確實是這樣的分佈。

而對於字符串常量則存放在.rodata段中,而且對於字符串而言還有一個特殊的地方,就是它在內存中只存在一份。下面給個代碼來測試:

#include<stdio.h>

int main(void)

{

const char *pStr1 = "hello,world";

const char *pStr2 = "hello,world";

printf("0x%x\\n", pStr1);

printf("0x%x\\n", pStr2);

return 0;

}

大家可以驗證一下,輸出的地址肯定是一樣的。因為常量字符串“hello,world”只存在一份。

2.什麼是指令

說完了數據,那什麼是指令呢?也就是什麼是程序代碼。很簡單,程序中除了數據,剩下的就都是指令了。這裡有一個容易混淆的地方,如下面的代碼:


#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("a+b=%d\\n", a + b);
return 0;
}

大家可能會有一個疑問,就是對於上面的代碼,a和b明明是局部變量,難道不是數據嗎?嗯,它真的不是數據,它是一條指令,這條指令的功能是在函數的棧幀上開闢四個字節,並向這個地址上寫入指定值。

3. 什麼是符號

說完數據和指令,接下來是另一個基礎而且重要的概念,那就是符號。我們在編寫程序完,進行鏈接時會碰到這樣的錯誤:"錯誤 LNK1169 找到一個或多個多重定義的符號",即符號重定義。那什麼是符號,什麼東西會產生符號,符號的作用域又是怎樣的呢?

在程序中,所有數據都會產生符號,而對於代碼段只有函數名會產生符號。而且符號的作用域有global和local之分,對於未用static修飾過的全局變量和函數產生的均是global符號,這樣的變量和函數可以被其他文件所看見和引用;而使用static修飾過的變量和函數,它們的作用域僅侷限於當前文件,不會被其他文件所看見,即其他文件中也無法引用local符號的變量和函數。

對於上面的 “找到一個或多個多重定義的符號” 錯誤原因有可能是多個文件中定義同一個全局變量或函數,即函數名或全局變量名重了。

4.虛擬地址空間佈局

對於32位操作系統,每個操作系統都有2^32字節的虛擬地址空間,即4G的虛擬地址空間。這4G的虛擬地址空間分為兩個大部分:每個進程獨立的3G的用戶空間,和所有進程共享的1G的內核空間。具體分佈如下圖:

從編寫源代碼到程序在內存中運行的全過程解析

二、編譯過程

1.編譯

整個編譯分為四個步驟:首先編寫源文件main.c/main.cpp;編寫好代碼以後進行預編譯成main.i文件,預編譯過程中去掉註釋、進行宏替換、增加行號信息等;然後將main.i文件經過語法分析、代碼優化和彙總符號等步驟後,編譯形成main.S的彙編文件,裡面存放的都是彙編代碼;最後一個編譯步驟是進行彙編,從main.S變成二進制可衝定位目標文件main.o。

以上四個步驟對應的在linux下的命令為:

gcc -E main.c -o main.i #預編譯,生成main.i文件

gcc -S main.i #編譯,生成main.S文件

gcc -c main.S #彙編,生成main.o文件

gcc main.o -o main #鏈接,生成可執行文件

2.二進制可重定位目標文件的結構和佈局

首先給出一個二進制可重定位目標文件(linux下是*.o文件,windows中是*.obj文件)的總體佈局,簡單來說整個obj文件就是由ELF header+各種段組成:

從編寫源代碼到程序在內存中運行的全過程解析

二進制可重定位文件的頭部,可以看到ELF header佔64個字節,裡面存放著文件類型、支持的平臺、程序入口點地址等信息,如果你對每個字段的具體含義感興趣,可以看《程序員自我修養》:

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是目標文件的各個段,從下面可以看到數據和指令在目標文件中是按段的形式組織起來的,而且.text段的起始位置從file off字段可以看到是0x40位置,即64字節處,也說明.text段是接在ELF header後面。

從編寫源代碼到程序在內存中運行的全過程解析

代碼段的大小為0x19,起始偏移為0x40,所以.data段的起始偏移應該為0x19+0x40=0x59,但是為了字節對齊,所以。data段的起始地址為0x5c,也即圖中file off字段所示,後面的段以此類推。

之後的.bss段會出現兩個問題,一個是.bss段的大小應該為4*6=24字節,但是實際上卻是20字節;另一個問題就是可以看到.comment段的偏移(file off)也為0x68,這說明.bss段在目標文件中是不佔大小的,即.comment和.bss段的偏移相同。對於這兩個問題,我這裡不作詳細介紹,簡單說一下。第一個問題,涉及到C語言中的強符號和弱符號概念;第二個問題我們可以這樣理解,因為.bss段中存的是初始化為0或者未初始化的數據,而實際未初始化的數據其默認值也為0,這樣我們就沒必要存它們的初始值,相當於有一個默認值0。

上面的圖只列出了部分段,下面查看一下目標文件中所有的段,一共有11個段,簡單說明一下,.comment是註釋段、.symtab是符號表段。

從編寫源代碼到程序在內存中運行的全過程解析

接下來就是看段的詳細內容,可以看到各個段真實的存儲內容如下,下面最明顯的是.data段,裡面存放著gdata1、gdata4和d的值分配為0x0000000a(10)、0x0000000b(11)和0x0000000d(13),正好與代碼中的初始值匹配。注意下面顯示的小端模式。

從編寫源代碼到程序在內存中運行的全過程解析

以上就是可重定位目標文件的組成,下面再介紹一下上面提到的符號表如下圖,第一列是符號的地址,由於編譯的時候不分配地址,所以放的是零地址或者偏移量;第二列是符號的作用域(g代表global,l代表local),前面討論了用static修飾過的符號均是local的(不明白的搜一下static關鍵字的作用),如下圖中gdata4/gdata5/gdata6等;第三列表示符號位於哪個段,在這裡也能看到gdata1、gdata4和d都存放在.data段中,初始化為0或未初始化的gdata2/gdata5/gdata6等都存放在.bss段:

從編寫源代碼到程序在內存中運行的全過程解析

這裡特別說一下gdata3,按上面的分析來說它應該是存放在.bss段,但是我們可以看到它是*COM*,原因在於它是一個弱符號,在編譯時無法確定有沒有強符號會覆蓋它。

以上就是編譯的詳細過程,不明白的歡迎大家留言,下面再來介紹鏈接。

三、鏈接過程

1.鏈接

鏈接過程分為兩步,第一步是合併所有目標文件的段,並調整段偏移和段長度,合併符號表,分配內存地址;第二步是鏈接的核心,進行符號的重定位。

(1)合併段

所有相同屬性的段進行合併,組織在一個頁面上,這樣更節省空間。如.text段的權限是可讀可執行,.rodata段也是可讀可執行,所以將兩者合併組織在一個頁面上;同理合併.data段和.bss段。

(2)合併符號表

鏈接階段只處理所有obj文件的global符號,local符號不作任何處理。

(3)符號解析

符號解析指的是所有引用符號的地方都要找到符號定義的地方。

(4)分配內存地址

在編譯過程中不分配地址(給的是零地址和偏移),直到符號解析完成以後才分配地址。如下圖,數據的零地址:

從編寫源代碼到程序在內存中運行的全過程解析

(5)符號重定位

因為在編譯過程中不分配地址,所以在目標文件所以數據出現的地方都給的是零地址,所有函數調用的地方給的是相對於下一條指令的地址的偏移量。在符號重定位時,要把分配的地址回填到數據和函數調用出現的地方,而且對於數據而言填的是絕對地址,而對函數調用而言填的是偏移量

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

從編寫源代碼到程序在內存中運行的全過程解析

從上圖中我們可以看到gdata1等變量的地址不再是0,而是0x080490e4,正確回填了絕對地址。

四、可執行程序

鏈接完成以後形成了可執行文件,下面來解析可執行文件是如何執行起來的。同樣,首先給出可執行文件的總體佈局,然後再來深入解析。

從編寫源代碼到程序在內存中運行的全過程解析

首先看一下可執行文件的頭部,如下圖,裡面記錄了函數的入口點地址為0x08048094(後面會解釋這個值的來由),還有就是size of this headers,程序頭部佔52個字節,然後還有三個program headers,每個program headers佔32字節,共佔3*32=96字節,所以程序頭部+program heades=52+96=0x94,而從虛擬地址空間佈局可知.text段正好是從0x08048000開始的,所以可執行程序的入口點就是0x08048000+0x94=0x08048094:

從編寫源代碼到程序在內存中運行的全過程解析

然後看看這三個program headers裡面的內容,第一個load項的屬性是可讀可執行,其實存放的就是代碼段;第二個load項的屬性是可讀可寫,其實存放的就是數據段。這兩個load項的意義在於它指示了哪些段會被加載到同一個頁面中:

從編寫源代碼到程序在內存中運行的全過程解析

可以看到這兩個load項的對齊方式是頁面對齊(32位linux操作系統頁面大小為4K)。

當雙擊一個可執行程序時,首先解析其文件頭部ELF header獲取entry point address程序入口點地址,然後按照兩個load項的指示將相應的段通過mmap()函數映射到虛擬頁面中(虛擬頁面存在於虛擬地址空間中),最後再通過多級頁表映射將虛擬頁面映射到物理頁面

從編寫源代碼到程序在內存中運行的全過程解析

說完編譯鏈接,最後說明如何將VP映射到PP就打工告成了。

分為三步,1.首先是創建虛擬地址到物理內存的映射(創建內核地址映射結構體),創建頁目錄和頁表;2. 再就是加載代碼段和數據段;3.把可執行文件的入口地址寫到CPU的PC寄存器中。

"

相關推薦

推薦中...