寫這篇博客就是想要解釋一個我自己在學習程序中比較困惑的問題:我們所寫的C語言代碼是如何在計算機中運行起來的?首先需要的知識儲備就是計算機里有哪些硬體,這些硬體之間是怎么配合著執行一條指令的,然后再分析,我們所寫的代碼有哪些指令構成,這些指令是怎么執行的,
一、計算機的硬體結構

說明:這里cpu內我沒有把暫存器單獨畫出來,而是將一部分主要的暫存器畫在了各個硬體里
總的來說:主存是用來存盤資料的,運算器是用來執行算術邏輯運算的,控制器可以控制各個硬體,負責從記憶體中取出指令,對指令進行分析,從而指揮指令有條不紊的執行,
具體來看各個部件:
1、運算器:運算器里主要執行算術、邏輯運算的是ALU,而ACC、MQ、X都是暫存器,用來存盤參與運算的運算元或者ALU執行運算后的結果,(具體哪些可以用來存結果,哪些可以用來存運算元等知識這里不贅述)
2、控制器:控制器里對指令進行分析進而控制運行的是CU,PC是程式計數器,存放的是下一條要執行的指令的地址,會自動+1,IR是指令暫存器,存放具體的指令
3、主存:也就是通常所說的記憶體,除了用于存盤資料的存盤單元以外,還有MAR和 MDR兩個暫存器,要想從記憶體中讀取資料,需要將資料的地址放進MAR中,記憶體再將MAR中地址對應的資料放進MDR中(寫操作類似)
二、各個硬體是怎樣配合著執行一條指令的
首先,我們所寫C/C++這樣的代碼會經過編譯、鏈接產生一個可執行檔案(二進制的機器指令)存在我們的磁盤中,在執行前會被加載到記憶體中(執行時二進制的機器指令和資料一樣,都是存放在記憶體中的,每一條指令也有其對應的地址)
以執行這條指令為例,康康各個硬體執行時市怎樣配合作業的:
y=a*b+c;
假設記憶體布局如下圖:

與之對應得執行步驟為:
1、pc=0(指向第一條指令),將pc的內容賦給MAR暫存器,使得MAR=0,且控制器告訴記憶體,此次執行讀操作
2、記憶體找到MAR存盤的地址所對應的存盤單元,將該單元內的資料放進MDR中,使得MDR=0000010000000101
3、MDR中所存盤的內容賦給IR暫存器,使得IR=0000010000000101
前三步完成了取指令操作
4、IR將操作碼部分(000001)傳遞給CU,由CU分析出該條指令是要進行取數
5、IR將運算元部分(0000000101)傳遞給MAR
6、記憶體找到與MAR存盤的地址對應的存盤單元,并將該存盤單元的內容放進MDR,使得MDR=0000000000000010
7、將MDR中的內容放入ACC暫存器中
此時,第一條指令執行完,pc=1(指向第二條指令),下面的步驟與上面類似都是:取值、分析、在控制器的控制下執行,在此不再贅述,
三、函式堆疊幀的創建和銷毀
上面分析了單獨一條指令在計算機中是怎樣執行的,但是我們平時所寫的C代碼都至少有一個主函式,函式在執行函式體指令之前,會創建函式堆疊幀,也就是說:我們所寫的C代碼,經過編譯之后所產生的二進制代碼,不僅僅包含函式體部分所對應的指令,還在函式體指令前面多出一部分指令,這一部分指令就是為函式創立堆疊幀用的,函式呼叫結束后創建的堆疊幀會自動銷毀,VS編譯器除錯環境中的反匯編,可以通過匯編代碼清楚的看到,函式堆疊幀創建和銷毀動作是怎樣完成的(本人使用的是VS2010,不同編譯器可能存在略微差異)
下面將通過一個例子分析來說明,函式堆疊幀創建、銷毀以及函式傳參的程序:
補充:push是壓堆疊操作,pop是出堆疊操作,ebp是堆疊底指標,esp是堆疊頂指標(會自動+1),可以通過下圖中呼叫堆疊視窗發現,main()函式是由_tmainCRTStartup()呼叫的,

假設 _tmainCRTStartup()呼叫main()函式前,堆疊底指標和堆疊頂指標如下圖:

開始執行main()函式之后,首先執行的三步是(匯編指令前的地址是該條指令的地址):
00641400 push ebp :將ebp內容壓入堆疊幀
00641401 mov ebp,esp :將esp內容放入ebp中
00641403 sub esp,0E4h :esp減去0E4h
執行完這3步之后,記憶體布局如下圖

00641409 push ebx
0064140A push esi
0064140B push edi :三個壓堆疊操作
0064140C lea edi,[ebp-0E4h] :將三個壓堆疊操作之前的堆疊頂指標加載到edi中
執行完這4步之后,記憶體布局如下圖
00641412 mov ecx,39h
00641417 mov eax,0CCCCCCCCh
0064141C rep stos dword ptr es:[edi]
這3步的作用是將main函式堆疊幀空間內的資料都改為:0CCCCCCCCh,這也就是為什么我們定義一個區域變數,如果不進行初始化,里面的隨機的為CCCCCCCC這樣的值,執行完之后的記憶體如下圖所示:
前面這些步驟執行完后,才開始真正執行我們所寫的代碼 (每句C代碼下面是其對應的匯編代碼)
int a=10;
0064141E mov dword ptr [a],0Ah
int b=20;
00641425 mov dword ptr [b],14h
int c=0;
0064142C mov dword ptr [c],0
這3步就是給a,b,c變數對應地址內的值改為變數的值(怎樣為變數分配地址跟編譯器有關,不同編譯器可能不同,其實我的編譯器兩個變數之間地址是相差8個位元組的,為了方便我畫成了4個位元組)

下面的代碼是呼叫add函式,但是通過下面的匯編代碼可以看出,在呼叫函式之前先傳遞引數,并且傳參的順序是由右向左
mov eax,dword ptr [b]
push eax
mov ecx,dword ptr [a]
push ecx

下面在除錯環境下按F11逐陳述句執行發現:在進入 add函式之前,call指令會先把下一條指令的地址壓堆疊,從我第一張截圖可以看到,call指令的地址是:0064143B,call指令的下一條指令的地址是:00641440,也即call指令會把00641440這個地址壓堆疊,然后進入add函式內,記憶體布局變為:

進入add函式后的匯編代碼如下圖:

函式體內 步驟與進入main()函式時一致,這里return z 這個操作是把z變數的值放入eax暫存器中,執行完之后,記憶體布局如下圖:
006413E1 pop edi
006413E2 pop esi
006413E3 pop ebx :三個出堆疊操作
006413E4 mov esp,ebp :堆疊空間銷毀
006413E6 pop ebp :出堆疊,內容放進ebp
006413E7 ret :來到call指令的下一條指令(之前將call指令的下一條指令地址壓堆疊就是為了函式呼叫結束后能回傳)
上述代碼執行完之后的記憶體布局變為:

緊接著回到call指令的下一條指令依次執行,本代碼中下一條指令為:
add esp,8 :銷毀形參
mov dword ptr [c],eax:將eax中存放的z的值,放入變數c中
執行之后記憶體布局為:

接下來就是main()執行結束后,堆疊空間的銷毀,跟前面add函式堆疊空間銷毀的步驟一樣,就不再贅述啦,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293653.html
標籤:其他
