hello world漫游
Hello world漫游
在進入我們今天的主題之前,我想回顧一下馮諾依曼體系結構以及存儲程序思想。太經典了!
1計算機是由運算器,控制器,存儲器,輸入輸出設備五部分組成
2采用存儲程序的方式,要執行的程序和數據先放到存儲器中
3采用二進制編碼數據
4程序是指令的集合,指令在存儲器中按執行順序存放
回到我們今天的hello world漫游,下面是我們要重點討論的部分,如果你確實看不下去,那么你也可以跳躍式閱讀。
1操作系統的啟動
2信息的表示
3程序的編寫
4 I/O設備的控制
5程序性能優化
6程序的編譯與鏈接
7存儲器層次結構以及虛擬存儲技術
8進程與異常控制流
9指令系統
10指令在硬件上執行
1:操作系統的啟動
首先在我們漫游之前,讓我們開啟我們的計算機,啟動操作系統,為hello world提供生存環境。操作系統的啟動過程主要是下面幾步,至于為什么我要介紹操作系統的啟動,接著往下看就明白了。
網上有很多詳細的介紹,具體可以參考下面鏈接,我不再詳述。
http://blog.chinaunix.net/uid-23069658-id-3142047.html
2信息的表示
Hello world 的生命周期可以說是從源程序的產生開始,說到源程序我們必須先對源程序有一個基本概念—信息。信息就是位+上下文(深入理解計算機系統),在計算機系統中信息是用位和上下文來表示的。以文件形式存在于磁盤上,包括.txt .cpp .asm .obj…。其中文件主要包括文本文件和二進制文件,可以這么理解,和計算機打交道的是二進制文件,和我們打交道的是文本文件。二進制文件就是由一串二進制位0和1來表示的。對于這樣的0和1我們是不怎么喜歡看的了。為了讓我們更簡單明了的看到計算機中的信息,計算機大牛們就想出了一個很巧妙的辦法來實現人機交互的問題。那就是編碼,對一串二進制位0和1進行編碼,常見的有ascii,unicode,gb2312…。也就是通過一串0和1來表示成我們能看的懂的信息。例如ascii中,用二進制的65D 0010 0001B表示大寫字母A,unicode中用\u6b22\u8fce表示中文 ”歡迎”。
編碼是很神奇的一步,也是實現人機交互很重要的一步,下面有興趣的同學可以繼續深入了解其他信息的表示方式和處理,如整數,浮點數,運算。說到數的表示,在計算機中同樣也可以理解為是一個編碼的過程。就如我們常說的,原碼,反碼,補碼一樣。想想我們平時使用的十進制原來也是一個編碼的過程啊,只是我們沒有過多想過這個問題,只是欣然的接收了而已,哈哈!
下面就簡單介紹下計算機中整數,浮點數,以及他們的運算問題。常見的數進制有二進制,八進制,十進制,十六進制,當然他們之間的轉換我就不作介紹了。我們生活中通常使用十進制,而計算機中使用二進制,但是考慮到十進制和二進制之間轉換的不易性。我們程序員在計算機表示中常用十六進制,如地址的表示我們不會用一長串0和1來表示,也不會用十進制表示,我們通常會用0XFFFF來表示。下面是C語言中常見的數據類型以及他的表示范圍。當然,不同計算機系統,對數據類型所占的字節數有所不同,但總是二進制表示的形式。
說到這里,對于一個稱職的程序員,我想有必要了解的就是,數據類型之間的轉換問題,
關于這個問題,我想最根本的解釋就是回到他們的二進制表示形式以及定義上來理解。下面就介紹強制類型轉化和截斷數字的準則。
1對于位不同的類型之間的轉換,就是對數據的位進行擴充或者截斷,當然擴充的方式需要根據數據的定義,有的是前補0,有的是前補符號位,截斷就是保留數據的低有效位。
2對于位相同的類型之間的轉換,就是根據數據類型的定義來重新解釋該數據(無符號數)。
數據的機器級表示中有大端對其和小端對其,這里需要注意的就是,小端法(高有效位放在地地址)大端法(高有效位放在地址),下面是1234H在不同機器下的表示。
s為符號位,表示正負
f為尾數,f是二進制小數,表示為0.的形式
e為階碼,e采用無符號數表示
IEEE浮點數表示有幾種情況:規格化,非規格化,特殊值
1規格化:e不全為0,也不全為1
S=s,M=1+f,E=e-Bias(單精度Bias為127,雙精度為1023)
那么 就等于
2非規格化:e全為0
S=s,M=f,E=1-Bias,換種理解,M=1+f,E=e-Bias也是一樣,問題的關鍵是保證數的連續性
那么 就等于
3特殊值:e全為1,表示值為無窮大,NAN
關于數的運算(算術運算,邏輯運算,關系運算,位運算),當然和我們生活中數的運算一樣,只是在計算機中同樣是采用二進制數運算,作為程序員,我們更關心的是如何安全,高效的進行運算。
1:有符號數和無符號數進行運算時,隱式轉換為無符號數對待
浮點數與整數進行運算時,隱式轉換為浮點數對待
2:乘法和除法運算,為了簡化運算時間,常轉換為位運算來代替
3:有符號數采用的是算術右移和無符號數采用的是邏輯右移
1有符號數和無符號數比較求值
2整數乘除轉位運算
如x*14表示為(x<<3)+(x<<2)+(x<<1)
3帶符號數的算術右移操作
好了,有了上面的編碼基礎,我們可以開始進行hello world源文件的編輯了。
3:hello world編輯
說到編寫源程序,我想大家一定會第一時間打開一個熟悉的編輯器,然后用不到幾十秒的時間寫出hello world代碼,接著編譯運行。然而很可惜的就是現在確實有那么些“程序員”只是在做這一部分工作,真是名副其實的“碼農”。今天,我想簡單從硬件角度介紹編輯源文件的問題,這也就是我為什么已開啟要介紹操作系統啟動的原因,因為只有操作系統啟動后,我們才能更好的通過操作系統來管理我們的硬件資源以及我們的應用程序。當我們打開一個編輯器時,實際上是在執行一個應用程序,操作系統會為這個程序創建進程,通過進程來管理我們的編輯文本文件時執行的操作。也就是說進程在等待I/O設備。當我敲擊鍵盤時很快會在編輯器行顯示如下源文件。但是硬件上,我們是怎么實現源文件的編輯的呢,接著往下看。
4:I/O控制方式
為了理解進程與I/O設備的數據交流,我們有必要先了解 I/O接口(適配器)。對于I/O接口,我們可以采用內存一樣的對其進行編址,編址方式可以采用I/O獨立編址,也可以與主存一起進行統一編址。編址之后,我們就通過I/O接口來訪問控制I/O設備。我們就能像訪問地址一樣的來訪問I/O設備,講到后面的文件系統之后,我們也可以把I/O設備看作設備文件。對I/O設備的操作就好像是對文件操作一樣。
1 程序查詢式
通過程序指令不斷的去查詢設備是否準備輸入/輸出數據,這對cpu來說是極大的浪費時間。多說無用!
2中斷
中斷是控制I/O設備的一個很重要的方式,同樣是一個非常值得借鑒的控制機制。講到中斷,我們先來了解下8088的中斷系統
其中,中斷的實現是通過8259A芯片硬件上來實現的,但中斷的管理也需要操作系統來實現。至于中斷嵌套可以自行看微機原理書。中斷包括硬件中斷和軟中斷,可屏蔽中斷和不可屏蔽中斷。需要注意的是中斷是一種機制,而異常,更非錯誤。當外設向cpu發出請求時,會在8259A芯片上產生一個中斷信號。cpu通過中斷邏輯得到中斷向量號,然后去訪問中斷向量表得到中斷處理程序的入口地址,在系統啟動后,中斷向量表存放在主存的從0000H—03FEH地址(8088),共255個中斷。由于中斷向量地址占4個字節,而中斷向量號是按序存放的,所以中斷向量地址也等于4*中斷向量號。
中斷響應過程(同jmp指令執行過程比較類似,至于保護現場和恢復現場只是一種簡單的描述,不過就是些寄存器狀態的保存,當然還是挺麻煩的),中斷處理過程的示意圖如下:
有了中斷控制方式,cpu就不用不斷的去查詢等待外設是否處于準備狀態,而是外設通過中斷來請求cpu了,但是考慮到外設每一次請求cpu就中斷一次的話,cpu需要的代價很大,不僅要停下來而且要保存現場,然后去執行中斷,最后恢復現場,才能繼續工作。所以又有了下面的DMA控制方式。
3 DMA控制方式
DMA全稱direct memory access也就是是直接在內存與I/O直接建立聯系,每次DMA先緩存一定量的數據塊,然后當數據塊滿了之后,向cpu申請占用總線,這時DMA直接讀寫內存,控制I/O與內存的操作,這樣就大大減少中斷的次數,提高了cpu的利用率。
4通道控制方式
通道控制方式同樣是采用硬件來實現的,采用I/O通道控制器,相對于DMA來說,通道控制器是一個簡易的處理器,能執行有限的指令集,能控制數據傳送的方式,如字節多路通道,選擇通道,數組多路通道。
5 思考
關于I/O設備基本概念就介紹這些,相信大家都有一定的理解。下面回想我們hello world的編輯,如果從根本出發,只要我們認為我們敲進去的不是鍵盤上的26個英文字母,而是敲進去一個中斷信號,然后程序調用中斷程序執行相應的中斷程序,我們的一切行為都是在為程序服務,是程序為我們在磁盤上創建文件,編輯文件,保存文件就ok了,同樣需要注意的我們的源文件是以ascii碼的形式保存的。好了,編輯完hello world源文件之后,我們就可以開始編譯運行了,相信很多C教材都是這么說的,但是作為程序員很有必要了解這個過程,因為在以后的工作中,我們經常會遇到類似的問題,我們常常會認為程序沒有問題,但是卻編譯不成功,或者是編譯不報錯,但運行不起來,類似的問題會讓我們頭痛不以,下面我們就慢慢的理解編譯鏈接的整個過程。
5:hello world的編譯與鏈接
1 預處理,把C語言中的一些預處理語句解釋出來,如#include,#define ifdef,也就是宏定義,文件包含,條件編譯。執行cpp命令,會在當前目錄下產生test.i文件,查看文件可以看出預處理所做的工作。我們會發現test.i文件里面引入了很多我們沒有編輯過的代碼。
2 編譯
說到編譯,就是把C語言程序編譯成匯編代碼。那么我們還是要有一定的匯編語言基礎,至少能看懂匯編代碼。當然,我們所學的匯編是inter匯編,與編譯器匯編出來的匯編指令語法不同,但是還是可以相通的,如果你還是很遺憾的說,匯編我都幾年沒用了,差不多都忘了,那也沒關系,下面一張常用匯編程序格式能給你簡單的回憶(這里我省去了宏,條件編譯等)。
看完上面的匯編程序的一般格式后,默認大家對匯編有一定的理解,至少能看明白。畢竟我們確實很少會再去寫匯編代碼了。然而不得不說的是匯編語言確實很強大,而且很危險,然后我只想對早期的程序員說,你們真的辛苦了,匯編代碼真難調試!!!
下面我們來對比一下,匯編語言寫的hello world,和C語言經過編譯后的hello world有什么不一樣
C語言版
匯編語言版
對比C語言匯編得到的匯編代碼和直接用匯編語言寫的代碼,我們發現兩者之間非常相似,但又有不同。其中我們注意到的是,C語言匯編后的代碼有很多.file .def類的標識,這些我們可以暫時理解為是匯編器產生的一種標識,方便后面進行鏈接用的。還有一個就是_call _printf的調用,這個我們暫時理解為是對函數庫的一個調用,在鏈接過程中繼續討論??梢钥吹贸鏊麄冎g很類似,雖然目前看他們輸出字符串執行的命令不同。
既然談到C語言和匯編,那就不可避免的得說一下棧的問題。因為C語言中的控制,函數調用同匯編中的過程控制都需要運用到棧。棧的一個基本思想就是先進后出。為了更好的理解C語言中函數的遞歸調用,我們這里用匯編語言來描述它對棧操作過程。
C語言實現代碼:
#include <stdio.h> int fu(int x) {if(x<1)return 1;elsereturn fu(x-2)+fu(x-1); } int main() {int x;fu(x);return 0; }機器反匯編代碼:(每一步的執行都對應一條匯編指令,真正用匯編語言實現不用如此麻煩,因為,匯編語言也支持選擇,循環,調用)
00401020 push %ebp 00401021 mov %esp,%ebp 00401023 push %ebx 00401024 sub $0x34,%esp 00401027 movl $0x401150,(%esp) 0040102E call 0x4019b4 <SetUnhandledExceptionFilter@4> 00401033 sub $0x4,%esp 00401036 call 0x401360 <__cpu_features_init> 0040103B call 0x401770 <fpreset> 00401040 lea -0x10(%ebp),%eax 00401043 movl $0x0,-0x10(%ebp) 0040104A mov %eax,0x10(%esp) 0040104E mov 0x402000,%eax 00401053 movl $0x404004,0x4(%esp) 0040105B movl $0x404000,(%esp) 00401062 mov %eax,0xc(%esp) 00401066 lea -0xc(%ebp),%eax 00401069 mov %eax,0x8(%esp) 0040106D call 0x40195c <__getmainargs> 00401072 mov 0x404008,%eax 00401077 test %eax,%eax 00401079 jne 0x4010c5 <__mingw_CRTStartup+165> 0040107B call 0x401964 <__p__fmode> 00401080 mov 0x402004,%edx 00401086 mov %edx,(%eax) 00401088 call 0x401520 <_pei386_runtime_relocator> 0040108D and $0xfffffff0,%esp 00401090 call 0x401750 <__main> 00401095 call 0x40196c <__p__environ> 0040109A mov (%eax),%eax 0040109C mov %eax,0x8(%esp) 004010A0 mov 0x404004,%eax 004010A5 mov %eax,0x4(%esp) 004010A9 mov 0x404000,%eax 004010AE mov %eax,(%esp) 004010B1 call 0x40134d <main> 004010B6 mov %eax,%ebx 004010B8 call 0x401974 <_cexit> 004010BD mov %ebx,(%esp) 004010C0 call 0x4019bc <ExitProcess@4> 004010C5 mov 0x4050f4,%ebx 004010CB mov %eax,0x402004 004010D0 mov %eax,0x4(%esp) 004010D4 mov 0x10(%ebx),%eax 004010D7 mov %eax,(%esp) 004010DA call 0x40197c <_setmode> 004010DF mov 0x404008,%eax 004010E4 mov %eax,0x4(%esp) 004010E8 mov 0x30(%ebx),%eax 004010EB mov %eax,(%esp) 004010EE call 0x40197c <_setmode> 004010F3 mov 0x404008,%eax 004010F8 mov %eax,0x4(%esp) 004010FC mov 0x50(%ebx),%eax 004010FF mov %eax,(%esp) 00401102 call 0x40197c <_setmode> 00401107 jmp 0x40107b <__mingw_CRTStartup+91> 0040110C lea 0x0(%esi,%eiz,1),%esi匯編語言實現代碼:
``` code segment;主程序main proc farassume cs:code start:mov ax,0 ;ax存儲函數值,清零mov bx,8 ;設置函數xpush bx ;參數壓棧call digui ;ip壓棧 retmain endp;遞歸子程序digui proc near push bp mov bp,sppush dx ;保存和的后一個加數push bxsub sp,2mov bx,(bp+4)cmp bx,2je out1cmp bx,1je out1sub bx,1mov (bp-6),bxcall diguimov dx,ax ;將和的第一個加數保存到dx,去計算后一個加數dec bx ;求f(n-2)push bxcall diguipop bx ;清空nadd ax,dx ;將第二個加數加到第一個加數上,即求和jmp out2 ;作為第二個加數的參數壓棧out1:mov ax,1 out2:add sp,2pop bxpop dxpop bpretdigui endp code endsend main
由此,我們可以看出函數的遞歸調用就是一個壓棧的過程,先把一些需要保存的寄存器壓棧,再把參數壓棧,壓棧順序為至右向左,最后是是返回地址壓棧,直到函數執行return返回,返回即對應一次出棧操作。同樣,我們可以看出匯編語言實現的麻煩,我們必須清楚的知道棧里面的情況,想想要是程序員一不下心搞錯了怎么辦?哈哈,這就引出我們編程上的數組越界和緩沖區溢出問題了。當程序的返回地址CS:IP被惡意修改,那么程序就會轉而去執行一段我們并不知道的程序,當這段程序是攻擊性程序的話,那么我們就麻煩了。當然現在的操作系統已經對此類問題又了很強的保護。所以說匯編語言是很強大,但也很危險的!
3 匯編
匯編是一個將匯編語言文件匯編成二進制文件的一個過程。這里我們再來看看匯編后的二進制文件,由于二進制文件不能直接打開,但是我們可以看到下面的東西:先匯編產生二進制文件,然后反匯編得到一個類似于.Lst的文件,其中左邊是對應的二進制機器碼,右邊為匯編代碼。
下面是匯編語言產生的.Lst文件,左邊為機器碼,右邊為匯編代碼
從上面二個文件中可以看出,C語言匯編后的二進制代碼和匯編語言的代碼很相似,唯一不同還是C語言的匯編代碼時通過_printf輸出hello world,而匯編語言是通過dos中斷 int 21來實現輸出hello world。_printf調用來哦什么呢,下面就讓鏈接的解釋問題吧。
4鏈接
想想,計算機能識別就是二進制文件,既然有了二進制文件,怎么還不能執行呢。對的,我們還不能執行,因為在此之前,我們還有很重要,很關鍵的事要做–鏈接。通常我們的程序都是分模塊的,就好像我們的.c中需要.h頭文件一樣。我們把各個功能模塊分開來,把一些大部分程序要用到的功能段都放到.h文件中,這樣,其他的c文件想用的時候都可以把他include進來。然而對于可執行文件也是一樣,我們也可以把一些大部分可執行文件需要用到的功能放到一個靜態庫,動態庫中,就好像一個模塊一樣,即插即用。我們常用的有靜態鏈接庫.a,和動態鏈接庫.dll。
例如,我們在編程過程中常遇到需要使用庫函數的問題,我們通常是采用include把需要的庫函數加載到我們的源文件中。那么編譯,鏈接后得到的可執行文件中就包括了我們需要的庫函數。但是有個問題我們得想清楚,include加載到源程序中不僅加載了我們需要的函數,還包括很多其他功能實現,盡管這樣不會增加我們.c文件的大小,但是它會大大增加編譯鏈接后得到的可執行文件的大小,這樣不僅浪費了磁盤存儲,更可怕的是他加載到內存中后,對內存是一種極大的浪費??紤]到上面的兩個問題,鏈接部分完美的解決了這兩個問題。
靜態鏈接.a,它是可重定位目標文件,采用下面的命令,我們同樣可以得到我們想要的可執行文件,而不需要把不需要的功能實現加載到可執行文件中,這樣就解決了磁盤存儲的浪費。
動態鏈接.dll,他是共享目標文件,他可以加載到任意的內存地址,并和一個在內存中運行的程序鏈接起來,也就是說多個程序在運行時可以共享這個功能實現。這樣它在內存中就不需要拷貝多份而浪費內存了,如printf。
那么,就會有同學會說,那為什么我們還是要用include stdio.h,下面是我的一個嘗試,把它去掉后,程序會出現警告,但還是可以正常運行,說明在我的系統路徑下一定有一個包含printf的動態鏈接.dll文件。
但是我們別喜過了,鏈接是給我們帶來了很大的方便,但要真的實現即插即用真的就這么簡單嗎,多個目標文件是怎么鏈接的,怎么插入的,并非是簡單的插入進去就行了。而是需要對文件的重整。下面我們介紹一下我們的目標文件。
通常我們的目標文件分為三類
1 可重定位目標文件
2可執行目標文件
3共享目標文件
對于上面的目標文件,都有一個典型的格式
我只簡單介紹幾個節的含義
.text: 已編譯的機器代碼
.rodata:只讀數據
.data: 已初始化的全局變量
.bss:未初始化的全局變量
.symtab: 符號表
.rel.text:鏈接器把這個目標文件和其他目標文件結合時,需要修改這些位置。
.rel.data:
有了上面的一些節之后,我們就能把多個目標文件鏈接到一個可執行文件中去。為什么我們要這么做呢,其中最重要的就是實現指令的執行順序的安排。這里我們提下后面要講的一個概念,就是虛擬地址。簡單的理解虛擬地址對應的是我們的磁盤地址。有了這個虛擬地址之后,我們就能合理的,有序的安排指令執行順序。為了看看鏈接后的虛擬地址,我們采用反匯編來看看鏈接之后得到的文件和前面的匯編文件有什么不一樣。
反匯編鏈接后的可執行文件得到的
直接反匯編二進制文件得到的
對比上面的,我們能夠看到的就是他們的指令和機器代碼完全一樣,但是最左邊的地址不一樣,這也就是鏈接過程所做的事了,當然,鏈接的代碼我只截取了一部分。
好了,終于講完了程序是怎么變成可執行文件的了,看到這里,相信你對源程序的編輯,編譯,鏈接已經有了自己的理解,一定也很累了。但是我們的漫游還沒結束,我們還需要深入計算機硬件,了解程序是怎么運行的。不過在此之前,我們先了解下程序運行的性能問題,相信對自己嚴格要求的程序員一定會買一本代碼規范化編程的書籍好好的研究程序優化問題,但這里我只是簡單說下,緩解一下疲勞!
6:程序性能優化
下面來看一看一個簡單的數組求和問題(下面是偽代碼)
下面是一個對結構體數據求和的過程
消除循環的低效率
減少過程調用
消除不必要的存儲器的引用如函數傳遞的參數是放在存儲器的棧中的,而局部變量可以放在寄存器中,訪問寄存器的速度明顯優于存儲器
循環展開
提高并行性等…下面我們進入計算機硬件,了解計算機體系結構。我們先看一張圖,大致了解一下計算機有哪些硬件組成。
7:存儲器層器結構以及虛擬存儲技術
同樣在我們運行可執行文件之前,我們必須要清楚一個概念,因為前面我們說到過指令的執行順序是很關鍵的,而我們使用的是虛擬地址,而不是真正的物理地址,執行前必須虛擬地址轉換為物理地址之,那么或許這時你會想為什么我們不直接使用物理地址呢?我們不妨來看下那就是虛擬存儲器。下面就來介紹一下我們的存儲器層次結構以及其中最重要虛擬存儲器。
關于各種類型的存儲詳細介紹,可以百度!
下面介紹兩種DRAM的擴展方式,位擴展和字擴展。
有了上面的這些存儲器之后,下面繼續介紹我們的存儲器層次結構
為什存儲器采用如此多的層次結構,為什么采用緩存和虛擬存儲技術,最根本的問題就是解決cpu與存儲器之間速度不匹配的問題,當然還有擴展存儲器大小的用途。也就是說,每一次cpu根據地址去訪問存儲器時,都是先訪問更快但又容量極小的存儲器。如果不存在,則繼續往下找。那么關鍵的問題就是高速緩存中能否找到cpu想要的指令或者數據,而我們的操作系統會盡最大努力的提高命中率。但是很可惜的是,我們不能未卜先知,預測cpu下一個需要訪問的地址。我們能做的就是先把最大可能訪問的東西放到高速緩存中,然后提高cpu隨機訪問命中的概率。但是幸運的是我們的程序具有時間和空間上的局部性,這就大大提高了我們cpu訪問緩存命中的概率。為了提高命中率,合理利用高速緩存空間就顯的十分重要了。硬件我們采用了各種存儲器映射方式,軟件上采用了各種的頁面置換算法。
存儲器映射方式
1全相連映射(不贅述)
2直接映射
3組相連映射
頁面置換算法(這里不過多介紹)
1先進先出
2最近最久未使用
3clock置換
如果說緩存是緩存主存到高速緩存的話,那么虛擬存儲器就是緩存磁盤到主存,前者主要加快了訪存的速度,那么后者就增大了主存的容量(虛擬內存)。這就是虛擬存儲技術,也就是為什么我們能運行比我們內存大的程序的原因,也是我們使用虛擬地址(邏輯地址)的原因。而且程序在很長一段時間使用的都是虛擬地址,只有真正執行的時候才會將虛擬地址轉變為實際的物理地址,簡單說吧,虛擬地址就是映射到磁盤的地址,物理地址是映射到內存的地址。那么讓我們的可執行文件運行起來,還有很重要的一步就是虛擬地址到物理地址的轉換。
下面就介紹虛擬存儲器,解決虛擬地址到物理地址的映射問題,當然高速緩存的地址轉換也是類似。虛擬地址翻譯,在進行虛擬地址翻譯過程中需要用到一個頁表,該頁表是存放在內存中的(物理存儲器),為了圖示,我把他單獨列出物理內存。
從上面可以看出,頁表就是一個定位指令或數據地址的作用,有效位表示是否在物理存儲器中,為0表示不在,為null表示該頁表項未被使用,為1表示能在物理存儲器上找到。
考慮到虛擬存儲器很大的時候,假如64位,那么如果我們采用上面的映射方式一個頁表項對應磁盤上4K大小。那么我們的頁表就需 s
可見這是不可能的。所以引出了多級頁表的方法來解決我們不斷擴大的虛擬存儲器問題。
也就是說一級頁表負責映射虛擬存儲器上的4M地址空間,二級頁表繼續劃分這4M空間,負責映射4K地址空間。這樣大大減少來哦頁表的地址空間,但是不得不說的是,增加了地址映射的復雜性。但這對高速的cpu來說不是問題,問題是需要多訪問一次物理內存。好了,講完地址映射之后,我們終于可以把我們的程序裝載到內存上運行了。
8:進程與異??刂屏?/p>
程序需要運行起來,沒有操作系統是不行,因為操作系統管理著我們的硬件資源。在計算機系統下,提高系統的吞吐量是操作系統的一大職責,早期的操作系統沒有過多的要求,只需要管理計算機硬件資源,保證有效性(提高資源利用率和系統吞吐量)即可,當然操作系統的目標有:有效性,方便性,可擴充性,開放性。所以早期的操作系統有單道批處理系統,多道批處理系統,分時系統,實時系統。無論如何,操作系統的目標就是上面幾個。由于程序不具備并發性,不能有效的利用計算機硬件資源,所以操作系統提出了進程和線程的概念。進程是一個抽象的概念,進程是程序運行的一個實例,也就是說,操作系統為了提高資源利用率,通過進程來抽象的執行程序。也就是說,我們的程序需要以進程的形式才能運行起來!
這里簡單介紹一下進程,每一個進程都有唯一一個進程標識號PID。操作系統在初始化的時候,有一個pid為0號的進程,它是所有進程的父進程。
操作系統會在下面幾種情況下創建進程。
1用戶登入:合法用戶登入后,操作系統為該用戶終端創建一個進程。
2作業調度:系統用來完成進程調度的一種機制
3提供服務:系統提供的一套為用戶進程的服務
4應用請求:應用進程申請創建一個進程
前面3個都是系統創建新的進程(內核態),第4個是用戶創建進程(用戶態)
下面我們開始運行我們的hello world程序。
windows下我們執行可執行文件test.exe,當然linux下是./test,但這不是問題的關鍵。問題的關鍵是運行hello world時操作系統做了些什么?這直接關系到我們繼續了解程序運行的實質。當我們在命令窗口執行test時,因為test不是內置的外殼命令,所以他會認為test是一個可執行文件,當外殼運行一個程序時,父外殼進程會調用fork()函數生成一個子進程,內核為新的子進程創建各種數據結構,并分配唯一的PID,為了給子進程創建虛擬存儲器(就是虛擬地址到物理地址的映射表)它創建與當前進程一樣的mm_struct,區域結構, 頁表的原樣拷貝。它是父進程的一個復制品。子進程通過調用execve系統調用啟動加載器。加載器會刪除子進程現有的虛擬存儲器,并創建一組新的代碼,數據,堆,和棧。通過虛擬地址空間中的頁映射到可執行文件的頁大小的片,新的代碼和數據段被初始化為可執行文件的內容,最后加載器跳到_strat地址,最終調用main函數。在加載過程中不會把程序的數據拷貝到存儲器,直到cpu引用一個虛擬頁面才進行拷貝,此時利用的就是操作系統的頁面調度機制。關于fork和execve函數就不做過多介紹,有興趣同學可以自己查閱資料,看看這兩個函數的實現和作用。當然,進程運行的過程并非總是一帆風順的,操作系統也不會讓一個進程一直占用cpu直到進程結束。進程運行過程總會發生一些異常,注意異常不是錯誤,可以理解為是一種機制。
異常的類別有下面幾種
1中斷
2陷阱和系統調用
3故障
4終止
異常的機制和中斷類似,也就是進程執行過程中,有其他更重要的事要做,cpu轉而做其他事情,實現機制也類似,中斷有中斷向量表和中斷處理程序,異常有異常表和異常處理程序,尋址方式和中斷也類似。所以進程的執行過程也可以用下面示意圖表示。
操作系統終于組織好我們的程序了,并以進程的方式開始運行了。離我們真正的在硬件上分析指令的運行就差最后一步了。
9指令系統
別忘了,我們的計算機可不認識什么程序,進程之類,計算機硬件只能識別0和1,不過幸運的是我們的前面通過lst文件也看到匯編后的二進制文件,左邊為機器碼,右邊為機器指令。所以這又是一個編碼的過程,也就是用一串0和1來編碼機器指令,我們只要知道指令系統的編碼規則就好,至于指令功能上的實現,那是硬件上的問題了。為了讓大家能記起我們的匯編代碼和機器碼,我再把之前的lst文件貼上,也方便介紹后面的指令系統和硬件執行指令的過程。
下面是深入理解計算機系統書中定義的一套Y86指令集,就是用一串0和1組成的二進制數代表一條指令。其實如果我們是搞硬件的話,我們也可以自己設計電路,定義一套簡單的指令系統集。
當然指令系統集的定義也必須合理安排,否則對硬件是一種極大的浪費和損害上面定的Y86指令集是由1-6個字節進行編碼的,也就是說不是固定長度編碼。不定長編碼將會導致執行不同的指令使用的總線數量不一致。下面是我們熟知的CISC和RISC指令系統集
10指令在硬件上執行
有了指令系統之后,我們就有了和硬件打交道的語言了。但是我們必須在硬件上實現指令系統的功能。要實現數字系統在硬件上需要有三個主要部分支持:1計算對位進行操作的函數的組合邏輯,2存儲位的存儲器單元,3以及控制存儲器元素更新的時鐘信號。幸運的是我們都有。
1邏輯門
下面是TTL電路實現上面邏輯門的硬件原理圖,具體怎么實現,我也看不明白了!
2存儲器:如我們的虛擬存儲器系統,寄存器(通用,專用,段)
3時鐘信號:存儲單個位或字節,時鐘控制寄存器加載輸入信號
如我們常用的運算操作指令,傳送指令。
計算機硬件翻譯指令過程
看完上面的圖,我們再回過頭來看看馮諾依曼體系結構以及存儲程序思想。
1計算機是由運算器,控制器,存儲器,輸入輸出設備五部分組成
2采用存儲程序的方式,要執行的程序和數據先放到存儲器中
3采用二進制編碼數據
4程序是指令的集合,指令在存儲器中按執行順序存放
好了,這一次hello world漫游就到這里,如果你能看到這里,并能理解我今天介紹的這些東西,那么相信你一定對計算機系統有一個系統的理解,一定會覺得閱讀的幾個小時有收獲!
參考資料
1深入理解計算機系統
2匯編語言程序設計
3C/C++程序設計語言
4計算機組織與結構
5計算機操作系統
616/32位微機原理
注:文章中圖部分來源于百度
總結
以上是生活随笔為你收集整理的hello world漫游的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Maven手工安装jar包到本地仓库
- 下一篇: sanity测试_Sanity.io入门