少點代碼,多點頭髮linux
本文已經收錄至個人GitHub,歡迎你們踊躍star 和 issues。c++
面試官超級喜歡問hello world問題 特別是校招,我校招碰到過3次github
其實不少看起來順其天然簡單的東西,背後是一套複雜的學問面試
記得很清楚第一次面試阿里巴巴的時候,面試官上來讓我寫一個hello world程序shell
當時我真的一面黑人問號的確認了三遍,面試官依舊淡定的說 是的 編程
寫完就讓我聊hello world,一個hello world聊了一個小時數據結構
那時候面試是校招實習,聊完我真的懷疑人生了 架構
這個問題很是考驗應試者的計算機基礎、自學能力以及對問題鑽研的能力app
要回答好這個問題,必須掌握計算機基礎、操做系統、編譯原理等知識才能給出一個完美的答案
來了,開聊了,還沒關注個人記得關注我,一鍵三連
代碼如上,如今看來很簡單 怎麼也不會想到這樣的程序還會出錯
不丟人的說,龍叔第一次在寫這段代碼的時候,這個簡單的程序大概寫了三四遍
好不容易倒騰完了,點擊運行後 發現少了頭文件
加上以後再運行,發現少告終尾的 ; 號
加上以後,發現少了return 0
就這樣倒騰了好幾遍,終於在控制檯輸出了hello world!!! ,那一刻我激動得笑出了聲
因而驕傲的我趕忙趁熱打鐵,寫了下面的版本
這兩個版本的代碼都是C語言寫的,C語言課程應該是大學的通識課了,用這個語言講,你們都能看的明白
運行結果:
外甥很是好奇,這hello world究竟是怎麼輸出到屏幕的
龍叔也好奇過這個問題,只不過是在C語言學完以後纔開始好奇
從馮·諾依曼的結構咱們能夠知道,計算機的基本組成部分以下:
程序,首先是經過輸入設備,鼠標、鍵盤輸入的
寫好的代碼在文本文件中,是須要存儲的,此時就用到存儲器,代碼是存儲在磁盤中的
當你點擊運行時,你的代碼會被讀到內存中,在內存中的代碼會通過編譯器進行編譯爲可執行文件
編譯後的文件經操做系統的進程去啓動一個用戶進程執行用戶的可執行程序
中央處理器會去處理程序邏輯,將執行結果輸出到輸出設備即顯示器
每一個部分都有本身的工做,恪盡職守,這個在系統設計上叫模塊清晰、功能完整
接下來就從幾個方面好好說說這個 hello world,讓面試官目瞪口呆下
代碼輸入這麼簡單的問題,還用龍叔講??
如上圖首先說下輸入過程,此圖作了一個濃縮,主要部件 鍵盤、主機(CPU、內存、磁盤)、顯示器
代碼輸入過程看起來是蠻簡單的,打開一個編輯器或者IDE,便可開始代碼輸入
剛開始學習推薦使用IDE,固然不是沒有IDE就不能寫代碼
任何一個文本編輯器均可以進行代碼輸入
IDE(Integrated Development Environment) 集成開發環境,通常包括代碼編輯器、編譯器、調試器和圖形用戶界面等工具
好比寫C&C with class 會下載 vc++、devC++、VS、Clion等等軟件,很棒,工具能提升生產力
我習慣用Clion,IDE都是根據本身的須要來選擇,用着爽就行
IDE是一個軟件,集成度很高的軟件 ,啓動IDE意味着操做系統必須啓動一個進程 該進程叫IDE進程
既然是集成 內部還有不少線程負責集成模塊的工做
關於進程、線程 深層次的內容,後面文章會詳細講出 這裏就先不展開了
IDE進程會被操做系統管理和調度
要明白這個問題得先說說鍵盤工做原理
鍵盤的基本原理就是實時監控按鍵,將按鍵信息送入計算機
在鍵盤的內部設計中有定位按鍵位置的鍵位掃描電路,當任何鍵被按下是 編碼電路就會產生代碼,這些代碼會被送入接口電路,這些電路被稱爲鍵盤控制電路
根據鍵盤工做原理,分爲編碼鍵盤和非編碼鍵盤
編碼鍵盤:鍵盤控制電路的功能徹底依靠硬件來自動完成 ,根據按鍵自動識別編碼信息
非編碼鍵盤:鍵盤控制電路的功能依靠 硬件 和 軟件 共同完成
監控鍵盤的原理就是電位掃描,電位掃描分爲逐行掃描法和行列掃描法
原來如此,原來鍵盤是這樣工做的,今後我在飛速敲擊鍵盤時 會更有力量了
這僅僅是鍵盤驅動進程拿到鍵盤輸入的結果,應用程序是如何得到輸入數據的呢?
鍵盤後臺進程拿到結果後會放在本身的共享內存中,應用程序經過共享內存獲取到鍵盤輸入結果
上圖中很明顯看到鍵盤輸入是會發生IO操做的,IO總體內容這裏不展開,後面文章會更新
一頓操做,此時IDE會拿到鍵盤輸入的代碼,你的hello world代碼終於在顯示器中讓你看到了
接下來講說躺在IDE中代碼是如何運行出結果的
代碼終因而敲好了,激動的你通常會想着要運行一手,火燒眉毛看到結果
別急再等等,咱們書寫的代碼程序被稱爲源代碼,CPU執行的是機器碼,這個包含機器碼的程序被稱爲可執行程序
先來看看源代碼是如何變爲可執行程序的
IDE是集成環境,很容易讓初學者覺得源代碼直接被CPU執行了
其實否則
源代碼必須通過編譯器編譯 才能成爲二進制的可執行程序
IDE裏面集成了 編譯器 調試器 ,C語言的編譯器 主要有GNU編譯器套件中的GCC、Microsoft C 或稱 MS C、Borland Turbo C 或稱 Turbo C
編譯過程是一個複雜的過程,接下來聊聊這個複雜的過程
編譯是個過程的總稱,其中還包括不一樣的階段,源代碼預處理階段、編譯優化階段、彙編階段、連接階段
預處理器將對其中的僞指令(以# 開頭的指令)和特殊符號進行處理,刪除全部的註釋,最後生成 .i文件
僞指令包括:
使用gcc命令能夠輸出.i文件
gcc -E helloWorld.cpp -o helloWorld.i
此時.i文件是刪除了註釋、宏替換、頭文件也加載進來了,該文件比源代碼文件大
內容太多,代碼就不粘貼了,你們自行試驗下
編譯程序所要做的工做就是經過詞法分析、語法分析、 語義分析,在確認全部的指令都符合語法規則以後,將其翻譯成等價的中間代碼或彙編代碼
詞法分析和語法分析千萬不要混淆了,校招面試的時候被面試官給繞了半天
詞法分析器識別出Token,把字符串轉換成一個個Token
Token包括關鍵字、標識符、字面量、操做符、界符等
爲何要這樣作呢,把代碼裏的單詞進行分類,編譯器後面的階段不就更好處理理解代碼了嘛
語法分析階段把Token串,轉換成一個體現語法規則的樹狀數據結構,即抽象語法樹AST
AST樹反映了程序的語法結構
好比hello world代碼通過語法分析以後會獲得一個AST樹
不少人疑惑爲何要把程序轉換成AST這麼一顆樹呢?
由於編譯器不像人能直接理解語句的含義,AST樹更有結構性,後續階段能夠針對這顆樹作各類分析
語義分析顧名思義就是理解語義,也就是理解程序要作什麼
好比理解 "+" 符號是執行加法、"="號是執行賦值操做、"for"結構就是去執行循環等等
那到底怎麼理解呢?
這個階段要作的就是進行上下文分析,上下文分析包括引用消解、類型分析以及檢查等等
引用消解:找到變量所在的做用域,一個變量做用範圍屬於全局仍是局部做用域
類型識別:好比執行a=3,須要識別出變量a的類型,由於浮點數和整型執行不同,要執行不一樣的運算方式
類型檢查:好比 int b = 3,是否能夠進行定義賦值,等號右邊的表達式必須返回一個整型的數據或者可以自動轉換成整型的數據,纔可以對類型爲整型的變量b進行賦值
通過語義分析後得到的信息(引用消解信息、類型信息),會在AST上進行標註,造成 帶有標註的語法樹,讓編譯器更好的理解程序的語義
在語法分析後有了程序的抽象語法樹,在語義分析後有了 帶有標註的AST 和符號表後,就能夠深度優先遍歷AST,而且一邊遍歷一邊執行結點的語義規則
對於解釋性語言整個遍歷的過程就是執行代碼的過程
解釋性語言如Python 等,在遍歷帶有標註和符號表的抽象語法樹便可開始執行
編譯性語言須要生成目標代碼,如C、C++
編譯型語言須要生成目標代碼,而解釋性語言只須要解釋器去執行語義就能夠了
以前校招面試的時候,面試官看我把hello world講的這麼好,順手問了句Java、Python 執行hello world的過程同樣麼?
當時愣了下,知道不同 可是沒解釋的很清晰
對於不一樣架構的CPU,生成的彙編代碼不一樣,若是優化是針對每一種彙編代碼,那這個過程就至關複雜了
因此在生成目標代碼以前增長一個過程,先生成一個 中間代碼IR,統一優化後再生成目標代碼
優化代碼主要從分爲本地優化、全局優化、過程間優化
本地優化:可用表達式分析、活躍性分析
全局優化:基於控制流圖CFG做優化
過程間優化:跨越函數的優化,多個函數間做優化
說了一些乾的,舉個例子讓你們理解下到底如何優化
活躍性分析就是將一些沒有用到的代碼刪除,好比一些沒有用到的變量
目標代碼生成就是將優化後的IR代碼翻譯爲彙編代碼
翻譯爲彙編代碼主要步驟是
編譯階段使用的指令
gcc -S helloWorld.cpp -o helloWorld.s
生成的彙編代碼:
用的GCC版本信息以下
上面的編譯階段的生成的彙編代碼仍是人能看懂的,不是給機器直接執行的,機器執行的叫作機器碼
機器碼放在可執行文件中
unix環境中存在好幾種目標文件:
不一樣的操做系統的可執行文件格式不一樣
彙編程序生成的其實是第一種類型的目標文件,連接完成以後才能生成可執行文件
將彙編階段生成的一個個的目標文件連接在一塊兒生成可執行文件
其實不少人不理解爲何須要連接這個過程,明明彙編階段已經生成目標代碼
舉個例子你們就明白了,平常作系統開發的時候,咱們講究系統功能模塊化 如今都是微服務
一個複雜系統,每每會分紅多個不一樣的子系統 子系統在拆分爲不一樣的功能模塊
連接的過程也和這個相似 一個複雜的軟件須要拆分爲多個不一樣的模塊,每一個模塊獨立編譯
根據須要在 "組合" 起來,這個組裝模塊的過程就是 連接
好比main函數中調用了printf函數,mian函數在編譯時並不知道printf函數的地址(每一個模塊都是單獨編譯的)
可是調用又必須知道函數地址才能發生調用關係
編譯時暫時把這個地址擱置,連接時在進行地址修正
連接完成以後會造成一個可執行文件 ,可執行文件也叫ELF文件
這個ELF文件以及其餘文件也夠喝一壺,放在後面講聊文件系統 一塊兒聊
)裝載就是把可執行程序加載到內存中,供後續的CPU執行
在linux命令行中咱們常常這樣執行一個可執行程序
./a.out
這樣一下就把程序加載到內存中,加載完成以後直接執行了
其實你可使用
strace ./a.out
這個命令能夠看到全部的系統調用
能夠看到 第一個執行的系統調用是 execve
經過 man execve 能夠看到這個函數的描述
execve() executes the program pointed to by filename. filename must be either a binary executable, or a script starting with a line of the form:
#! interpreter [optional-arg]
execve()執行文件指定的程序 文件必須是二進制可執行文件,或者執行一個以 shebang開頭的腳本
Shebang 就是 #!
開頭
經過查看Linux的execve源碼以下
主要執行工做落在了 do_execve
上,繼續看看 do_execve 源碼
前面就是計算一些參數如argv、env 拷貝相關數據,最終裝載程序執行search_binary_handler
list_for_each_entry
函數很是重要,這個函數遍歷全部formats列表,找到當前系統合適的可裝載格式
前面已經說過,linux 下可執行文件格式是ELF文件
retval = fmt->load_binary(bprm)
就是load可執行程序
load_binary是加載二進制文件啊,咱們的程序明明是ELF文件
仔細看看load_binary的源碼會發現裏面有一個初始化,初始化的時候會作一個賦值替換爲
或許到這裏你們基本已經瞭解了,但仍是疑惑怎麼才能判斷加載的ELF文件
能夠去看看源碼怎麼寫的 (源碼太長,這裏就不粘貼了 告訴你位置有興趣的本身去看看)
源碼位置:
有個函數叫 static int load_elf_binary(struct linux_binprm *bprm);
在 /fs/binfmt_elf.c Line 820
再看看咱們的可執行程序頭上長啥樣 readelf -l a.out
便可查看可執行文件頭部信息
解釋器經過判斷 Program Headers 中的 INTERP 的值獲得該可執行程序的文件類型
咱們的CPU執行程序的步驟是:
上面步驟是一個循環也稱爲CPU指令週期,CPU 的工做就是一個週期接着一個週期,周而復始。
更多關於CPU執行的問題,能夠看看好朋友小林的 你很差奇 CPU 是如何執行任務的?
或者持續關注,後面我會更新關於CPU執行調度的文章
在Unix系統中,每一個進程都會默認打開三種標準I/O 分別是STDIN、STDOUT和STDERR
printf源碼
這只是第一次源碼,願意瞭解的能夠看看vfprintf實現,你會發現底層使用了 緩衝輸出
輸出是一次output,也就是會經歷一次從內存外部文件系統的數據轉移
到這裏基本就講完了了hello world所有內容,講完了不必定是講透徹了
好比 關於文件系統的知識、IO知識、CPU調度知識、進程管理、內存管理等等知識都無法經過一篇文章說透徹
說實話一個小小的hello world藏着大學問,囊括的內容也實在是太豐富了
今天只是從總體上把控了一下,細節內容後面寫操做系統會一一更新
我是龍叔,咱們下期見