【施磊】C++語言基礎提高:深入學習C++語言先要練好的內功


課程總目錄


文章目錄

  • 一、進程的虛擬地址空間內存劃分和布局
  • 二、函數的調用堆棧詳細過程
  • 三、程序編譯鏈接原理
    • 1. 編譯過程
    • 2. 鏈接過程


一、進程的虛擬地址空間內存劃分和布局

任何的編程語言 → \to 產生兩種東西:指令和數據

編譯鏈接完成之后會產生一個可執行文件xxx.exe,會把程序從磁盤加載到內存中,不可能直接加載到物理內存!!!

環境: x86 32位linux環境

程序:

int gdata1 = 10;
int gdata2 = 0;
int gdata3;static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;int main()
{int a = 12;int b = 0;int c;static int e = 13;static int f = 0;static int g;return 0;
}

linux系統會給當前進程分配一個 232(4G)大小的一塊空間(進程的虛擬地址空間),大小和環境的位數相關,如果是64位,則為8G

在這里插入圖片描述

注意區分虛擬內存虛擬地址空間,這是兩個不同的概念

  1. 0x00000000 ~ 0x08048000
    這段無法被訪問,如果運行char *p = nullptr;strlen(p);則會報錯,因為空指針在這段區域,char *src = nullptr;strcpy(dest, src);也會報錯

  2. 0x08048000 ~ 0xC0000000

    • .text(代碼段): 放指令只讀)。main函數中的三個初始化 a, b, c 語句,都會轉化為一條mov指令,如mov dword ptr[a], 0xCH,如果cout << c,此時的c是什么不確定(參考文章),它是棧上的無效值;int main(){}以及cout << c << g << endl;都是指令,都存放在 .text

    int a = 12; 這條語句不產生符號,只產生對應的匯編指令,對應指令存放在 .text上,但是當指令運行的時候,指令做的是在棧上開辟4字節的空間將12放進去

    • .rodata: 只讀數據read only。char *p = "hello world";其中p在棧上,常量字符串"hello world"就存儲在 .rodata段,但是如果*p = 'a';,通過指針讓常量字符串的第一個字符修改為a,可以編譯但不能運行,因為這一部分是只讀的
    • .data(數據段): 用于存儲已經初始化并且不為0全局變量和靜態變量,這些變量在程序運行之初就有了確定的初始值,在程序執行之前就會被初始化,因此需要分配實際的存儲空間。 [gdata1 & gdata4 & e]
    • .bss: 用于存儲未初始化和已經初始化為0全局變量和靜態變量[gdata2 & gdata3 & gdata5 & gdata6 & f & g]

    此時cout << gdata3 << endl;輸出為0,因為gdata3存放在 .bss段。操作系統會把沒初始化的變量全部置為0

    • .heap:堆
    • 加載共享庫:在window系統中是*.dll,在linux中是*.so
    • stack:棧,函數運行或產生線程時,產生的棧空間,從下往上(高地址向地地址)進行增長
    • 命令行參數和環境變量

在 Linux 中,進程在內存中一般會分為五個段,包含了從磁盤載入的程序代碼以及其他數據。即代碼段、數據段、BSS段、堆、棧

  • 0xC0000000 ~ 0xFFFFFFFF
    • 內核空間

在這里插入圖片描述

每一個進程的用戶空間是私有的,但是內核空間是共享的。例如匿名管道通信,就是在內核空間中分配出一部分內存,進程1往里寫內容,進程2和3都能看見。

二、函數的調用堆棧詳細過程

int sum(int a, int b)
{int temp = 0;temp = a + b;return temp; 
}int main()
{int a = 10;int b = 20;int ret = sum(a, b);cout << "ret:" << ret <<endl;return 0;
}

問題一:main函數調用sum,sum執行完后,怎么知道回到哪個函數
問題二:sum函數執行完,回到main函數后,怎么知道從哪一行指令繼續運行

在這里插入圖片描述
程序分析:
int a = 10; → \to mov dword ptr[ebp-04H], 0AH
int b = 20; → \to mov dword ptr[ebp-08H], 14H
int ret = sum(a, b);編譯后會將位置為ptr[ebp-0Ch]命名為ret,之后是調用函數,先從右向左向棧頂壓入形式參數a和b,同時esp也會隨之移到棧頂,即

mov eax, dword ptr[ebp-08H]
push eax
mov eax, dword ptr[ebp-04H]
push eax
call sum  // 函數調用指令,會做兩件事,將下一條命令的地址(0x08124458)壓棧,進入sum
 // sum函數返回后
add esp, 8   // 本條指令地址(假如地址為0x08124458)將給形參分配的地址交還給系統
mov dword ptr[ebp-0CH], eax   // 將結果放到ret中

由此也可見,在函數調用過程中,形參的內存開辟是在調用函數時就分配好的

進入sum函數,在int temp = 0;執行之前,即左括號{int temp = 0;之間,會執行下面的匯編代碼

push ebp  // 此時ebp指向main函數棧幀的棧底,把此地址記錄下來
mov ebp, esp  // 把esp賦給ebp,此時ebp指向sum函數棧幀的棧底
sub esp, 4CH  // 給sum函數開辟棧幀空間

int temp = 0; → \to mov dword ptr[ebp-04H], 0
temp = a + b;

mov eax, dword ptr[ebp+0CH]  // 取形參b的值存到eax
add eax, dword ptr[ebp+08H]  // 取形參a的值,和b相加,存到eax
mov dword ptr[ebp-04H], eax  // a+b結果存到temp

return temp; → \to mov eax, dword ptr[ebp-04H]

右括號},回退棧幀

mov esp, ebp  // 把ebp賦給esp,把棧空間歸還給系統,但并未清空棧中內容
pop ebp  // 出棧,并把棧里的數值給ebp,即退回main函數棧幀的棧底,同時esp+4
ret  // 出棧,把出棧內容(0x08124458)放在CPU的PC寄存器中,同時esp+4

返回main函數中

 // sum函數返回后
add esp, 8   // 本條指令地址(假如地址為0x08124458)將給形參分配的地址交還給系統
mov dword ptr[ebp-0CH], eax   // 將結果放到ret中

之后再打印,return,結束程序

注:

數值 ≤ 4B,通過eax寄存器帶出
4B < 數值 <= 8B,通過eax和edx兩個寄存器帶出
數值 > 8B,函數調用之前產生臨時量,再把臨時量地址入棧,被調用函數return處通過偏移ebp訪問臨時量。

三、程序編譯鏈接原理

編譯過程: 預編譯 → \to 編譯 → \to 匯編 → \to 二進制可重定位的目標文件(*.obj / *.o)

鏈接過程: 編譯完成的所有.o文件 + 靜態庫文件(Linux下是*.a,Windows下是*.lib)
兩個核心步驟:(1)所有.o文件段的合并;符號表合并后,進行符號解析
???????(2)符號的重定位(重定向)【鏈接的核心】

最終在工程目錄下 → \to win下得到xxx.exe,Linux下得到a.out

我們需要關注的點:

  1. *.o 文件的格式組成是什么樣子的?
  2. 可執行文件的組成格式是什么樣子的?
  3. 鏈接的兩步做的是什么事情?
  4. 符號表的輸出 → \to 符號,符號怎么理解?
  5. 符號什么時候分配虛擬地址(在用戶空間上)?

程序:
main.cpp:

//引用sum.cpp文件里面定義的全局變量以及函數
extern int gdata;
int sum(int, int);int data = 20;int main()
{int a = gdata;int b = data;int ret = sum(a, b);return 0;
}

sum.cpp:

int gdata = 10;
int sum(int a, int b)
{return a+b;
}

1. 編譯過程

C++文件預編譯編譯匯編二進制可重定位的目標文件(*.obj / *.o)
main.cpp
sum.cpp
處理#開頭的命令語法分析、語義分析、詞法分析、代碼優化
g++ -O 0/1/2/3 指定優化等級
編譯完成之后生成特定架構下的匯編代碼main.o
sum.o

預編譯階段:#pragma lib 和 #pragma link 例外,不是在預編譯階段完成的,而是在鏈接階段完成的,這倆是用于處理鏈接階段的外部庫文件

現在來看我們的程序

首先進行編譯g++ -c xxx.cpp
在這里插入圖片描述
符號表:匯編器在把匯編碼轉成最終的.o文件時就會生成一個符號表

看一下符號表objdump -t xxx.o
在這里插入圖片描述

可以看到左邊全為0,即編譯過程中符號不分配虛擬地址,在鏈接過程中分配虛擬地址

分析:
在這里插入圖片描述

如果引用了外部文件,也會將外部文件中的符號產生在自己的符號表中。如果定義了main函數,則在符號表中函數的符號就是函數名,放在.text(代碼段);定義了全局變量data且值為20不等于0,因此放在.data(數據段);引用的gdata也產生了符號gdata,sum也產生了符號_z3sumii,但他們都是*UND*,這是符號的引用,而不是符號的定義。

sum.o文件的符號表中中,需要由函數名字和形參列表一起產生符號,例如這里的sumii解釋為sum_int_int

符號表的第二列,l表示locallocal的符號只能在當前文件中看見;g表示globalglobal的符號在其他文件也看得見。因此在鏈接時,所有.obj文件在一起鏈接,鏈接器可以看見所有global的符號,但看不見local符號。

.o文件的組成,可以用readelf -S main.o打印段表,用readelf -h main.o打印文件頭(節頭部表):

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

回答問題1:*.o 文件的格式組成是什么樣子的?
答:由上圖可見,是由各種段組成的(elf文件頭 .text .data .bss .symtab 等等)

編譯完成后,.o文件代碼段放入的指令如下,此時符號的地址位置填充的是0,這也是.o文件無法運行的原因之一,可以用objdump -S main.o打印代碼段
在這里插入圖片描述

2. 鏈接過程

步驟一:

  • 所有.o文件段的合并:在鏈接過程中,就要將main.osum.o的各個段進行合并,如.text段和.text段進行合并,.data段和.data段進行合并,.bss段和.bss段進行合并。包括段表和符號表,全部都進行合并。
  • 符號表合并后,進行符號解析:所有對符號的引用,都要找到該符號定義的地方。從原本的*UND*找到對應的在.text.data上的定義。如果鏈接器沒有找到對引用符號的定義,會報錯“符號未定義”;如果找到多個對符號的定義(重定義),會報錯“符號重定義”在符號解析成功后,給所有的符號分配虛擬地址。

步驟二:

  • 符號的重定位(重定向):將代碼段中的對應符號地址修改為為其分配的虛擬地址。

鏈接器指定入口并進行鏈接ld -e main *.o,其中-e是指定main作為入口,這樣在鏈接生成的輸出文件a.out文件的文件頭會將main函數的第一行地址401000作為入口點地址進行記錄

objdump -t a.out

在這里插入圖片描述

可以看到所有符號都分配地址了,都放到對應的位置了

objdump -S a.out

在這里插入圖片描述

readelf -S a.out

在這里插入圖片描述

回答問題2:可執行文件的組成格式是什么樣子的?
答:由上圖可見,可執行文件也是由各種段組成的

readelf -h a.out

在這里插入圖片描述

可以看到這是可執行文件,入口是main函數的第一行地址401000

readelf -l a.out

在這里插入圖片描述

可執行文件的段和重定向文件的段幾乎一致,只是多了一個program headers段,可用readelf -l a.out打印。運行可執行文件的時候,program headers段中LOAD哪些段,就是告訴系統把哪些段加載到內存中,如上圖,一般會將.text段和.data段加載到內存中

運行一個可執行文件:

  • 加載哪些內容 → \to 看program headers段
  • 從哪里開始運行 → \to 文件頭中的入口地址
    在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/14333.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/14333.shtml
英文地址,請注明出處:http://en.pswp.cn/web/14333.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python畢設項目選題匯總(全)

各位計算機方面的畢業生們&#xff0c;是不是在頭疼畢業論文寫什么呢&#xff0c;我這給大家提供點思路&#xff1a; 網站系統類 《基于python的招聘數據爬蟲設計與實現》 《基于python和Flask的圖書管理系統》 《基于照片分享的旅游景點推薦系統》 《基于djangoxadmin的學生信…

LeetCode hot100-47-N

105. 從前序與中序遍歷序列構造二叉樹給定兩個整數數組 preorder 和 inorder &#xff0c;其中 preorder 是二叉樹的先序遍歷&#xff0c; inorder 是同一棵樹的中序遍歷&#xff0c;請構造二叉樹并返回其根節點。這題放選擇題里還能選出來&#xff0c;前序中序一起確定了一顆什…

Linux備份服務及rsync企業備份架構(應用場景)

備份服務概述 備份服務:需要使用到腳本,打包備份,定時任務. 備份服務:rsyncd服務,不同主機之間數據傳輸. 特點&#xff1a; rsync是個服務也是命令使用方便&#xff0c;具有多種模式傳輸數據的時候是增量傳輸 增量與全量&#xff1a; 全量 &#xff1a;無論多少數據全部推…

貪心算法:合并區間

參考資料&#xff1a;代碼隨想錄 題目鏈接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 做過用最少數量的箭引爆氣球和無重疊區間這兩道題目后&#xff0c;題意和題解都不難理解。唯一的一點兒難點是對于api的運用。 class Solution {public int[][] merge(int[][…

設備管理全解析:從選購到報廢的全方位指南

在現代企業快速發展、智能化運營過程中&#xff0c;企業設備管理是保障生產連續性和效率的核心環節&#xff0c;其重要性不言而喻。然而&#xff0c;許多企業在設備管理內容流程方面仍然使用傳統管理辦法&#xff0c;這不僅影響了生產效率&#xff0c;也增加了不必要的成本。那…

vuejs路由和組件系統

前端路由原理 createRouter * hash* window.addEventListener(hashChange)* 兩種實現路由切換的模式&#xff1a;UI組件&#xff08;router-link&#xff0c;router-view&#xff09;&#xff0c;Api&#xff08;push()方法&#xff09; * history * HTML5新增的API &#xff0…

每日一題(1)

在看一本08年出版的書的時候&#xff0c;看到了這樣一個問題&#xff0c;感覺答案很奇怪&#xff1a; public class demo_p22 {public static void main(String args[]){int sCook1,sFish2;//各技能標記character ch1new character();if(ch1.haveSkill(sCook))System.out.print…

仙樂健康科技股份有限公司「E立方仿生增效技術平臺」推出新品啦

在這個看臉的顏值經濟時代,很多女性將肌膚保養作為人生的“必修課”,從各種網上攻略到高端護膚品,再到美容院的專業護理,可以說是應有盡有。最近,仙樂健康科技股份有限公司「E立方仿生增效技術平臺」推出的新品——PQQ鹽膠原小分子肽樺樹汁飲品,就受到了頗多追求健康美人士的關…

svg中漸變色的應用

設置漸變色背景 在 SVG 中可以使用<linearGradient>或<radialGradient>元素來設置漸變背景色。以下是一個簡單的示例&#xff1a; <svg width"400" height"400"><defs><linearGradient id"myGradient"><stop…

Discord運營攻略 | 從0-1教你搭建用戶社區!

Discord&#xff0c;這個最初為游戲愛好者設計的通訊平臺&#xff0c;現在已經發展成為了一個多元化的社區聚集地&#xff0c;涵蓋了各種興趣和行業。如果你是一名社媒運營人員&#xff0c;正在考慮如何從零開始構建一個充滿活力的Discord用戶社區&#xff0c;那么你來對地方了…

【CSP CCF記錄】202012-2 期末預測之最佳閾值

題目 過程 思路 第一次沒用前綴和&#xff0c;暴力求解得50分。 采用前綴和方法。 1. 對原數組stu[i]進行排序。 2. 計算前綴和數組s[]&#xff0c;s[i]表示安全指數的y_i的前綴和&#xff0c;即安全指數小于等于y_i時的實際掛科情況&#xff0c;y_i之前有多少個未掛科&am…

無線領夾麥克風哪個品牌好?無線麥克風品牌排行榜前十名推薦

?在當今的數字化浪潮中&#xff0c;個人聲音的傳播和記錄變得尤為重要。無論是會議中心、教室講臺還是戶外探險&#xff0c;無線領夾麥克風以其卓越的便攜性和連接穩定性&#xff0c;成為了人們溝通和表達的首選工具。面對市場上琳瑯滿目的無線麥克風選擇&#xff0c;為了幫助…

Python筑基之旅-MySQL數據庫(三)

目錄 一、數據庫操作 1、創建 1-1、用mysql-connector-python庫 1-2、用PyMySQL庫 1-3、用PeeWee庫 1-4、用SQLAlchemy庫 2、刪除 2-1、用mysql-connector-python庫 2-2、用PyMySQL庫 2-3、用PeeWee庫 2-4、用SQLAlchemy庫 二、數據表操作 1、創建 1-1、用mysql-…

38. 外觀數列 - 力扣(LeetCode)

基礎知識要求&#xff1a; Java&#xff1a;方法、for循環、if eles語句、StringBuilder類 Python&#xff1a; 方法、for循環、if else語句、字符串拼接 題目&#xff1a; 「外觀數列」是一個數位字符串序列&#xff0c;由遞歸公式定義&#xff1a; countAndSay(1) "…

記錄Python低代碼開發框架zdppy_amcrud的開發過程

實現新增接口 基礎代碼 import env import mcrud import api import snowflakeenv.load(".env") db mcrud.new_env()table "user" columns ["name", "age"]async def add_user(req):data await api.req.get_json(req)values [d…

SkyEye對接CANoe:助力汽車軟件功能驗證

01.簡介 CANoe&#xff08;CAN open environment&#xff09;是德國Vector公司專為汽車總線設計而開發的一款通用開發環境&#xff0c;作為車載網絡和ECU開發、測試和分析的專業工具&#xff0c;支持從需求分析到系統實現的整個系統的開發過程。CANoe豐富的功能和配置選項被OE…

虛擬ECU:徹底改變汽車軟件開發與測試

汽車開發領域有著垂直性較強的一系列需求&#xff0c;其中最為矚目的需求之一就是對安全高效的軟件測試方法的需求。傳統的汽車開發偏向使用硬件原型與真實ECU進行軟件測試&#xff0c;但由于硬件設備往往在開發周期的中后階段才生產完成&#xff0c;給汽車開發帶來了成本與時間…

理解Solidity 中的 tx.origin 和 msg.sender

開發者需要了解在Solidity中tx.origin和msg.sender的區別。這兩個全局變量經常被混淆&#xff0c;盡管它們之間有著根本的不同。雖然乍一看它們可能相似&#xff0c;但在交易的上下文中&#xff0c;tx.origin和msg.sender代表不同的地址。在這篇博客文章中&#xff0c;我們將深…

spring boot 之 事務

內容是小老弟的一些整理和個人思考總結&#xff0c;知識的海洋那么大&#xff0c;有錯誤的話還請諸位大佬指點一下&#xff01; 事務是一個不可分割操作序列&#xff0c;也是數據庫并發控制的基本單位&#xff0c;其執行的結果必須使數據庫從一種一致性狀態變到另一種一致性狀…

電商內卷時代,視頻號小店憑借一己之力“脫穎而出”

大家好&#xff0c;我是電商笨笨熊 今年618各大電商平臺花樣百出&#xff1b; 某寶更是直接取消了“預售”&#xff0c;從5月就開始進入618預熱期&#xff1b; 不少玩家既開心又難過&#xff0c;市場如此內卷&#xff0c;618確實是個爆發期&#xff0c;但更多的需要不斷壓低…