如何開始編碼

首發於個人博客網站(prajna.top) 歡迎你們前去交流,有pdf版本。node


本文主要是從應用的角度出發,分別闡述操做系統接口,計算機語言,文件系統等背後的一些知識,規範,原理,設計思想,應用法門,讓初學者對編碼有一個總體的,全局的認識,有一個物理的視角,找到本身的起點。python

前言

寫這篇文章主要是基於本身大學的經歷,當時抱着一腔熱血去學計算機編程,但是當把c/c++語言,數據結構,操做系統,計算機組成原理等課程都學完後,卻發現本身彷佛什麼也不會,只會printf打印一些字符串。那段時間真的好苦惱,特別想作軟件,殊不知從何開始,也不知道該如何去使力,蹉跎了很久,浪費了大量的時間。linux

形成這種現象的主要緣由,一是本身缺乏那種天賦,二是教學過於側重基礎和理論,每門課程只涉及到一個局部,沒有一門課程把這些串起來。我不瞭解語言的基礎庫,除了printf後,其它API都不會用;也不瞭解具體的操做系統平臺的API;雖然學了TCP/IP,socket的具體使用卻又不清楚; 至於像fat,ext等磁盤文件系統的格式,那就更遙遠了;我甚至還不清楚計算機語言和編譯工具的關係;更要命的是,我還不知道本身不知道這些。說白了就是理論同應用脫節,雖然大學也安排了課程設計,實驗和實習,但都只是走了過場,也沒有人來指點一下,該看些什麼書籍。大學讀完,就知道拖拉幾個控件作一個窗口,鏈接一下數據庫。c++

windows + VC屏蔽了太多的技術細節,惋惜大學期間接觸的恰恰就是它,對用戶這個是好事越傻瓜越好,但是對計算機的學生就要了命了。自從我轉投到linux開源世界後,終於才發現了什麼是自由,什麼是編程。當閱讀linux源碼碰到不理解的地方,能夠直接修改源碼加上打印,分析kernel流程。對比着minix的源碼來學習操做系統結構原理,那些概念就變成實實在在的數據結構和算法,動手寫一個minix的驅動,微內核和宏內核區別就一目瞭然了。強烈建議,想學習編程的同窗都去擁抱開源世界,而後,再回到本身感興趣的或工做相關的領域。程序員

計算機語言說白了就是工具,關鍵仍是你要作什麼,這樣就涉及到了應用,以及專業背景知識。如想作驅動編程,離不開對操做系統驅動架構的瞭解;想作一個磁盤分區合併,那須要瞭解文件系統的格式;作個播放器吧,那對視頻文件格式,編碼格式,編解碼API的瞭解必不可少。 隨着你對軟件系統瞭解的深刻,會發現其實一切都是協議。 http 是一套web 通信的協議;計算機語言是開發工具提供的協議; 操做系統是內核空間與應用空間的協議..., 這些協議被各類規範約束--並造成了各類技術。 因此,每種技術的背後都一套協議,規則和思想。瞭解這些才能算真正瞭解了相關技術。web

在這篇文章裏面,我以GNU/Linux做爲平臺,從應用的角度出發來把相關的課程來串一串提供一個「物理視圖」,讓初學者有個全局的認識,可以有一個方向和切入角度,至少知道該找些什麼資料來看。算法

操做系統接口

它是內核對應用空間提供的一套協議,主要包括:數據庫

  • ABI可執行文件的結構。
  • 系統調用(system call)。
  • sysfs 文件系統接口(linux kernel)。

ELF是編譯, 連接生成的,執行的時候,由ld 解析,加載在到內存,最後控制權交給程序入口代碼,程序開始執行。所以,它提供了2類視圖:連接視圖和執行視圖。 編程

0.png

從連接視圖上看,ELF由衆多的 Section組成,編譯器先把源碼編譯成.o文件,主要是提取函數,全局變量等生成符號表,把它們填充到相應的 Section裏面去。 在這個階段,全部的符號都是無法定位到地址的。windows

0.png

Link的時候,對.o文件進行合併,對各個文件內的符號進行重定位,安排它們的地址,以下圖所示, link完成後,g_u8 和 g_flag2都有地址了。
0.png

對於動態連接的函數,在link階段無法安排地址,須要放到 dynsym Section裏面去,在 ld的時候,來進行定位 -- 這就是所謂的 "函數重定位"。
0.png
​linux系統提供了可執行程序readelf來解析 ELF文件格式,咱們可使用它來了解一下ELF文件的一些通用的Section。

  • bss 是沒有指定初始化值的數據, 有些編譯器會默認所有初始化爲0。
  • data 全局變量初始化。
  • text 代碼。
  • init 程序初始化運行的代碼。
  • fini 程序結束運行的代碼。
  • symtab 符號表, 它是源碼編譯生成的產物,能夠爲代碼運行,調試提供信息。
    屬於輔助性質,不參與 load 和運行, 能夠用 strip來刪除掉。

'offset Align' 是各個Section在ELF文件內的偏移地址,咱們以二進制的方式打開ELF文件,根據偏移地址,就能夠查看相應Section的二進制內容。
0.png

從下圖中能夠看到 .interp的內容是 "/lib64/ld-linux-x86-64.so.2",
0.png

上面這些就是編譯,連接生成ELF文件的過程:編譯器以源文件做爲輸入,先提取各個文件的全局變量和函數,生成符號表,再把它們連接到一塊兒,連接的時候對各個符號進行定位,分配地址。對於動態連接庫的函數,則推遲到'加載'程序到內存的時候進行定位。編譯連接後,代碼和數據分散到了相應的section裏面,程序加載的時候,須要把Section 合併成Secgment,而後,以Secgement爲單位加載到內存頁面裏面去,咱們來看一下Segment的結構。
0.png

ELF有9種Segment,其中比較核心的是 --

  • INTERP, 用來指定誰執行這個ELF文件,
  • LOAD, 分別是文本(代碼)段和數據端。

Segment同Section 是有對應關係的,如:

  • INTERP -- .interp
  • LOAD(文本) -- .interp .init .text .fini .rodata 等。
  • LOAD(數據) -- .init_array .fini_array .data .bss 等。

程序執行的時候,先從INTERP 段找到對應的執行程序--可執行程序通常是ld.so, 首先加載ELF文件,根據Prgrame Header 數據結構,把section加載到各個程序段裏面, 而後,遞歸重定位動態連接符號,加載這些符合依賴的動態連接庫,處理ELF文件中的重定位, 而後,把控制權交給代碼入口,程序開始運行。在這個過程當中,ld 最重要的一個事情就是'重定位', 修改ELF裏面的動態連接函數的符號表。

系統調用(system call)

系統調用是kernel提供給應用層的API,經過軟中斷來調用。調用的形式是這樣
0.png
對ENTER_KERNEL宏的定義,傳統(i386)的調用方式是 'int $80', ia64是 'syscall'指令來進入kernel。 軟中斷的流程基本上都是這麼幾個步驟

  • 保存現場。
  • 把參數壓入相應的寄存器。-- kernel有定義它們的對應關係。
  • 寫系統調用號到 %eax 寄存器。
  • 調用 int $80 或者 syscall進入內核。 --不一樣的CPU會有不一樣的指令要求。
    獲得結果,恢復現場等。

咱們能夠在'arch/x86/syscalls/'目錄下找到 syscall的列表,
0.png

最前面的數字就是系統調用號, 如: 'syscall 5'調用的是'open'。 總共大約是400個左右,涵蓋了最基本的應用:如上圖中文件相關的操做, 進程類的(fork, execve)等。 這些系統調用都被libc庫作了封裝, 一些簡單的底層函數(如:mount, mkdir, stat)則只是簡單地被包裹了一下,直接軟中斷到kernel了。因此,你們若是想了解文件系統相關API的實現,必定要看kernel的源碼,看 libc庫的源碼是沒用的,它都是簡單地作了一個系統調用的轉換。

前面提到過一切都是協議,POSIX是一個普遍被支持的協議(規範),Linux和各種unix都對它提供了支持,只要操做系統申明支持POSIX接口,它就得實現POSIX定義的系統調用。對linux/unix而言, POSIX只是它們的一個子集,它會還會支持UNIX世界的一些系統調用規範。總之,有了這些規範,libc就能夠在各個系統間無縫移植。

sysfs 文件系統接口

sysfs是linux kernel以文件系統的方式提供給應用層的接口,在linux的世界裏,驅動模塊都被抽象爲文件系統節點,所以,咱們對 /sys/文件系統進行讀寫操做,能夠與內核裏面的驅動層進行交流。 具體的接口內容,請查詢 '/Documentation/ABI/'目錄下sysfs-module文件

計算機語言與編譯器

計算機語言就是一套人機交互的協議,比如咱們學了英語,就能夠同「支持」英語的人交流,來達到咱們的一些目的。計算機語言的本質也是同樣的,程序員經過某語言來調配它的資源,完成目標任務。不管是什麼語言,語法方面都大同小異,無非就是變量定義,表達式加幾個循環而已--它的理論源頭就是大名鼎鼎的圖靈完備的編程語言。圖靈從理論上論證了,只要符合圖靈完備規則,就能夠知足全部的自動化計算的須要。

所以,各類語言的語法都大同小異,那麼,爲何咱們還須要那麼多的語言呢?
這就回到了「什麼是計算機語言的本質「這個問題了,語言的核心究竟是什麼呢? 很顯然不是語法,而是它編程思想和資源(功能)。 每種語言都是爲了解決某些問題而存在,都會提供一套語法和平臺資源--包括標準庫和第三方庫。

彙編語言主要是爲了方便記憶,對機器指令上作了一個替換,沒有彙編語言,咱們還須要一邊查着芯片手冊,一邊手敲着 "6F 01 20" 之類的代碼。因爲它沒有提供內存管理和系統結構化的手段,本質上仍是機器指令級別的編程。使用匯編語言,咱們還得規劃內存,挑選寄存器,完成堆棧操做,所以,彙編只適合代碼量少的系統。如很小的單片機或者系統的引導代碼。高級語言則不一樣,編譯器幫忙搞掂了內存佈局,進棧出棧等這些煩瑣的事情,直接面嚮應用。

C語言提供了面向模塊的結構化思想,經過模塊化機制,咱們能夠分工合做,構建起應用。學習C語言,就是要了解它的模塊化思想,學會如何利用數據結構和函數指針封裝模塊,提供統一對外接口。另外,還得了解它的庫,這個決定了咱們能得到多大的資源支持。C的基本庫對系統調用進行了封裝, 提供了一些跟操做系統相關的函數(文件操做,進程管理,內存相關,socket等),它沒有提供經常使用的數據結構和算法(好比:鏈表的構建和排序,二叉樹),須要本身處理。 在應用層面,它則提供了字符和字符串處理,數學函數(sin,cos,tan,...),日期和時間等等,可見C語言的標準庫提供的都是底層的函數,固然了也有一些上層的GUI框架利用C語言提供接口的,他們在C的基礎上提供大量的擴展,一步步地架構了本身的系統。

C++雖然是C的擴展,但它是全新的語言,提供的是面向對象的架構思想。只不是兼容C語言規範,它能夠利用C語言的全部資源。它利用模板來提供標準庫(STL)封裝了經常使用的數據結構和算法。 官方又提供的 Boost模板庫,擁有了更豐富更強大的功能。 利用STL/Boost和基礎C庫,都足以開發一些底層軟件。 C和C++的基礎庫都沒有提供圖形,多媒體,GUI框架,用戶須要使用第三方的資源,如:SDL, opengl,Qt等。

像JAVA這類帶虛擬機的語言最厲害的地方,除了跨平臺性,就是它強大的類庫。不但封裝了經常使用的數據結構和算法,還集成了GUI框架,圖形庫,多媒體處理,也有很強的web處理能力。

python則提出不要重複造"輪子",由於它提供了大量可用的輪子,從基礎的數據結構到大型的應用模塊,應有盡有。幾句代碼就能夠完成一個 http服務器。 並且,python把變量定義這個環節都省了,直接面對要解決的問題,效率極高,特別適合教學,科研,作算法分析和原型驗證,工具軟件。 你想一想,如今你忽然有了一個算法構想,立刻想驗證一下,使用的python的話,你直接就能夠寫算法代碼了,想當於把算法的僞碼拿過來直接就用了。

用C語言的話,你得先定義各類數據結構,變量,再編譯,排錯,真的是很會很急人的,若是涉及到字符串的處理,心早就拔涼拔涼了。
軟件編程就是利用語言來調配各類資源來實現目標。學習一門新語言,除了學習語法,更多的關注點仍是它提供的編程思想和它的平臺資源。你得了解目標語言提供的各類資源,瞭解它的適用場景,它是爲解決什麼問題而存在。最好的學習方法是看一些經典的源碼,如:mplayer的C語言代碼,很好地詮釋了什麼是模塊化設計,真的使人歎爲觀止,會發現原來本身根本就不會寫代碼。

學彙編的比C語言厲害些嗎?
常常會聽到這樣的爭論,有一次地鐵上,我在用C++編碼,旁邊一個」哥們「問我學習C++是否是賺錢些,我唯苦笑不已。語言自己沒有高下之分,會用這個語言來完成任務纔算厲害,最厲害的就是作出產品,像 freebsd, linux 這種劃時代的產品,纔是真的厲害。對程序員而已,厲害的是你的編程思想和算法能力,行業的專業知識。語言這種東西,只是是信手拈來,實現你的想法而已。

編譯器就是語言的某一個具體實現,廠家不一樣,產品不一樣,像經典的 TC 和 VC。 編譯器最基礎的功能就是把所支持的源語言,編譯成操做系統支持的可執行文件格式。若是,它再提供一個GUI界面,增長了源碼編輯,斷點調試,GUI框架,那麼,它就成了一個開發平臺,好比經典的VC。 目前,咱們使用的集成開發環境都是融合了編輯,編譯,項目管理,資源編輯,GUI控件佈局,代碼生成等一系列工具。

對於初學者仍是建議你們本身使用GCC來做編譯器, GDB來調試, 本身寫Makefile來定義編譯規則,再找一個源碼編輯工具像Emacs或者Eclipse之類,這樣你們能夠很清晰地知道本身在幹什麼,須要作什麼。再使用集成開發工具的時候,知道該怎麼去處理各類問題,否則一直稀裏糊塗地不不知道一個'Run'點下去,到底發生了什麼,一出問題就傻眼。

若是須要重量級GUI框架的,能夠考慮Qt平臺,初學者也不要使用Qt Creator,能夠本身使用Qt的工具來作預處理,如:
0.png

能夠定義相應的規則,把它放到Makefile裏面,這樣就可使用make來進行管理。

數據結構和算法

話說學好了數據結構和算法,就基本解決了編程問題。但是當咱們拖拉幾個控件,寫幾個事件,就能夠完成工做的時候,不由會有點疑惑,說好的數據結構和算法呢。--其實,它無處不在,在應用框架裏,在中間件裏,在API裏面,在 Kernel裏。 C++的STL和JAVA都把一些經常使用的數據結構封裝成了「集合」對象。
數據結構就是對客觀對象的抽象,算法則是如何來組織和調用這些數據。在kernel裏面,咱們耳熟能詳的概念都是一個個具體的數據結構如:進程,內存頁面等等。下面是linux的進程數據結構'struct task_struct'部分片段,能夠看到對進程的描述信息(屬性)都定義在該結構裏面了。
0.png
這個就是咱們經過ps命令看到的進程信息描述,其實在linux kernel都是按照線程來管理的,因此,task_struct 也是線程的描述。 linux 源碼裏面就是大量的這種數據結構定義,以及把它們連接起來進行管理的各種鏈表,二分查找樹。

下面簡單說下數據結構和算法在STL裏面的應用。
STL做爲C++的標準庫,它封裝了經常使用的數據結構和算法。<vector> 的數據結構本質上是動態數組,當空間不夠的時候,它從新分配空間,並拷貝舊元素到的數組。

  • 「讀」的時間複雜度是常量--能夠隨機訪問。
  • 「插入」 須要額外進行數據拷貝,時間複雜度是 O(n)--n是pos到end元素數量,這部分數據須要拷貝。
  • 「push_back」的操做時間經平攤後是常量級別,vector在插入元素時,每每會額外多分配一點空間,以避免每次插入元素都進行「重分配」和「拷貝」的操做。 使用vector最好能夠顯示地給它分配空間,以提升性能。

<list> 是一個鏈表,「讀」和「 插入」的時間複雜度是O(n)--n是元素的位置, 「插入」數據操做的時間消耗是常量級的。

雖然<vector>的'插入'時間複雜度也是O(n),可是,<list>不須要拷貝後面的元素,只須要移動到相應的位置,它的'插入'性能更好,而<vector> 提供隨機訪問能力,適合經過'數組下標'直接訪問場合。

有沒有像<vector>那樣提供隨機訪問能力,可是又能提供 <list> 那樣的插入性能的數據結構呢?
有,那就是 <deque>,它至關因而 <vector> 和 <list> 的一個結合,至關於 <list> 來鏈接固定大小的<vector>。

圖片來源於 stackoverflow.com

  • 「讀」的時間複雜度是常量--能夠隨機訪問。
  • 「插入」的時間複雜度是O(n)--n是元素的位置。

那是否是可使用<deque>來代替<vector>和<list>呢? 固然不能夠:

  • <deque>的內存浪費會比較嚴重,那怕只有一個元素也會分配一整個數據塊。
  • <deque>的隨機訪問效率仍是不如<vector>高。
  • <deque>在某個chunk的插入也還面臨着拷貝末端數據的問題,所以,嚴格意義上說,它的插入性能仍是不如<list>。

<set>和 <map> 一般用一棵紅黑二叉樹來作爲數據結構的實現,根據關鍵字來排序,「查找」,「插入」和」「刪除」的時間開銷都是 O(lg(n)),它裏面的記錄是有序排列的,咱們根據關鍵字把它導出來就是一個有序列表,它們的綜合性能比較好。

若是須要更快速的訪問能力,能夠考慮使用帶hash結構的 <unordered_set> 和 <unordered_map> 「查找」,「插入」和」「刪除」的平均開銷是「常量級」的,性能很好,可是它裏面的數據是「無序」的。

文件系統

文件系統就是用來對文件進行增刪改查的控制系統,不一樣類型的文件系統對應着不一樣的管理策略,可是,不管何種策略都須要解決兩個最基本的問題:

  • 文件存儲。
  • 目錄訪問。

所以文件系統須要在磁盤記錄一些額外的信息,因此格式化之後,磁盤容量會小於實際的容量。格式化的過程就是在磁盤上「安裝」文件系統的過程,下面以EXT2和FAT32爲例來講說文件系統是如何工做的。

EXT

ext系列是linux下面主打的文件系統,它以Block爲容量單位,以Inode來抽象文件(目錄也是文件)。一個硬盤分紅多個Block Group,每一個Group裏面分別存儲Inode和具體的文件數據,以下圖所示:
圖片來源於 science.unitn.it

  • Super Block: 又稱超級塊,是文件系統的頭部--如同視頻和圖像文件都有一個獨一無二的頭部同樣:它包含文件系統的標識(magic代碼0XEF53),Block的大小(1k,2K,4K或者8K),Inode數量,Block數量,Inode大小等等文件系統的元數據。一旦它出現損壞,整個文件系統就將沒法識別,那麼這個文件系統就損壞了,裏面的數據也就丟失了。
  • Group Descriptors: 全稱 Block Group Descriptor Table簡稱GDT,包含全部的Group結構,用來描述每一個Group裏面Inode和Block的位置,空閒數量等信息。
  • Block/Inode Bitmap: 用一個bit位來標示是否空閒,如:bit15表明對應的第15個Block/Inode,0表示空閒,1則標示已被佔用了。
  • Inode Table: 每一個Inode表明一個文件,它存儲着該文件的屬性和位置,經過它就能夠訪問對應的文件。每個Block Group都有相同數量的Inode,所以,格式化後,所能支持的最大文件個數和每一個文件的最大容量都被決定了--Inode的數量就是所能支持的最大文件數量(包含目錄,目錄也被抽象爲文件),Inode的支持的最大尋址空間也就是文件的最大長度。
  • Data Blocks:文件數據的地址,是一個長度爲15的數組。

從上面圖中能夠看到,每一個Group Block的結構都是如出一轍,除了Data Blocks外,內容也都是同樣的嗎?
由於Super Block和 GDT是很是重要的,因此,每一個Block Group都有一份備份,所以他們的數據是同樣的,因爲備份會浪費空間,新版本的EXT系統再也不要求每一個Group都進行備份了,格式化的時候,能夠選擇備份策略。除了這兩項外,其它的內容就跟該Group的存儲的數據有關了。

那麼劃分Block Group的好處有哪些呢?
主要是爲了防止文件存儲碎片化。在存儲的時候,預先保留多餘空間,儘可能把文件放到一個Group裏面,所以文件不是互相連續存放的:好比前後建立2個文件,第一個文件放在213 Block,第二個文件可能就510 Block,預留一部分空間給文件擴展用。固然了,對超大的文件,是會跨Group存放的。 經過精心的Block Group劃分,再加上Linux靈活的文件分配策略,EXT文件系統在正常使用狀況下「碎片率」是比較低的。

目錄結構

目錄表沒有放到Inode Table裏面,而是放到Data Blocks上了,每一個目錄節點都對應一個Inode,它的數據區域(Data Blocks)就是它的目錄表。每一個目錄表項包括:文件名,類型和Inode號--經過Inode號又能找到下一級目錄表,這樣就構成了一個目錄鏈表結構。

類Unix系統都有一個「根目錄」,是路徑的起始點,系統就是從根目錄開始來遍歷目錄鏈表的,它也至關於鏈表的「根節點」,所以它的位置必需是固定的:Linux系統的「根目錄」Inode節點號是2。

下面以一個小容量的ext2系統爲例來遍歷下目錄。
首先查找「根節點」,它的內容以下:
0.png

BLOCKS: (0):204 表示:該Inode只佔用了一個Block(編號從0開始起),內容在204號Block上,咱們如今讀取Inode 2 裏面的內容(也就是204號Block):
0.png

根據目錄表項的定義解析二進制數據,就能夠獲得根目錄的目錄表:

Name Inode Type
. 0x02 目錄
lost+found 0x0b 目錄
home 0x501 目錄
large.img 0x0c 文件

目錄表項的結構

接下來,遍歷下一級目錄。
若是咱們要訪問"/home"目錄,經過查找表,會發現它的Inode號是1281, 而後,讀取Inode 1281的內容,再根據BLOCKS項的信息,讀取它的數據塊,又能夠獲得'/home'的目錄表--就如同前面讀取「根節點」同樣,只是Inode 2變成了Inode 1281。這樣反覆遞歸,就能夠一級一級地查詢到最終的目錄或者文件。

以上就是目錄存儲和查找的過程,目錄表存儲在Inode的數據區域,查找就是從根節點開始,反覆遞歸,直到目的路徑。

爲何「根目錄」的Inode號是2,而不是1或者0呢?
Inode 0 表示該Inode 不存在,相似C語言的空指針,這個是爲了編碼方便。 Inode 1是用存儲壞塊的, 因此,「根目錄」就是Inode 2了,固然了,不一樣操做系統的具體實現估計會有差別。

讀取文件內容

咱們經過目錄結構找到文件對應的Inode節點後,就能夠讀取它的內容了。Inode節點使用一個數組來存儲文件所佔用的Block號(BLOCKS項的內容), 數組的長度爲15,Block號用4個字節來表示。數組的前12位是當即尋址,第13,14和15位則是間接尋址。
下面以Block大小爲1024個字節爲例,來講明這個尋址過程。

0.png

如上圖所示:

  • 前12位是直接尋址,數組對應的內容就是文件的前12塊。
  • 第13位是「一重間接尋址」,它的內容不是文件內容,而是文件的塊地址--相似於C語言的指針,咱們先找到218塊,再根據218塊上的256個地址(1K/4)來讀取文件內容,也就是說這個地址的容量是256塊,範圍是[13 ~ 268]。如圖中所示,第13塊的內容在525塊上,1164塊則是文件的第268塊--這256塊不必定是連續存放的。
  • 14位是「二重間接尋址」,意味着連續2遍尋址,才能讀取到文件內容,所以它支持的容量是(256*256=65536)塊,可見它至關於C語言裏面的「指針的指針」。咱們先找到236塊,獲得它上面的256個地址,再依次讀取這256個地址獲得最終的65536個塊地址。從圖中能夠看到,236塊上的第一個塊地址是220,220塊上的第一個塊地址是1165--也就是說該文件邏輯上的第269塊存儲在1165號塊上,依次方法遍歷,能夠讀取該文件的65536塊的內容。
  • 15位是「三重間接尋址」,想當於C語言裏的「指針的指針的指針」,它支持尋址(256256256=16777216)個塊,就不肇敘了。

分析一下:文件最多可佔用(12+256+256256+256256*256 =16843020)個Block,若是Block的大小是1K,那麼文件大小的上限約是 16G。對於小於13個Block的文件是直接尋址,訪問速度快。間接尋址,主要是爲了增大文件的容量,同時也能快速讀取前12個Block的內容。

FAT

FAT是Windows下的一款經典文件系統,當時Windows系統下必定要作兩件事情是:碎片整理和殺毒,而這些都同FAT文件系統有關。下面先了解下FAT系統的基本內容,下圖是FAT的「物理視圖」。
0.png

BPB (啓動引導區域)-- 至關於FAT文件系統的「頭部」,定以了FAT文件系統的「元數據」:扇區大小,簇的大小,FAT表的位置和大小,根目錄的位置,基本上FAT文件系統的物理佈局,都定義在這個裏面,在系統格式化的時候生成。

FAT--全稱是File Allocation Table(文件分配表),實質上就是一個大數組,以「簇」爲單位 ,來記錄硬盤空間的使用狀況:

  • FAT12,每一個FAT表項是12 bits (1.5個字節)。
  • FAT16,每一個FAT表項是16 bits (2個字節)。
  • FAT32,每一個FAT表項是32 bits (4個字節)。

Untitled.png

目錄結構--目錄也被看成(抽象)成一種文件,它的內容就是一個目錄表。目錄項的定義以下:

Untitled.png

根據上面信息,能夠畫出FAT16物理佈局圖,

  • DIR_name: 長度只有11個字節--這是DOS時代「經典」的文件名過長的緣由,FAT32經過「長文件名」擴展解決了這個問題。
  • DIR_FstClus(HI/LO): 數據的起始地址,再經過FAT表去查找後續的地址。
  • DIR_FileSize: 文件大小, 4G的文件大小限制就是從這裏來的--由於它的長度只有4個字節。

下面咱們以一個只有10M的FAT16文件系統爲例,來實際說明一下FAT文件系統的工做過程。首先,解析BPB區域, 獲得FAT16的「元數據」,部分重要信息以下:
0.png

根據上面信息,能夠畫出FAT16物理佈局圖,
0.png

由於根目錄是特殊的數據,也能夠說數據區是從0x9200開始的。

讀取根目錄

接下來找到根目錄,讀取它的目錄表:
0.png

根據目錄項的定義,獲得根目錄內容以下:
Untitled.png

  • ROOT_F~1: 「~1」代表文件名過長被截了。
  • 起始地址: 第一個目錄項的'DIR_FstClusLO'(0x523A)開始的2個字節是0300,因爲是低字節在前(little endian),因此地址是0x03,而不是0x0300。同理,第二個目錄項目的地址是0x0d。

計算文件起始位置

文件地址的物理簇號是從2開始算起的,所以,ROOT_F~1.IMG的起始物理地址(以字節爲單位)計算以下:
文件起始地址: 0x9200 + (3 - 2) * 2048 = 0x9A00。

  • 0x9200是根目錄除外的數據區域起始地址。
  • 2048是每個簇的大小。

0x9A00就是文件的起始地址,當咱們讀完該簇後,該如何去尋找下一個簇呢?

查找FAT表

FAT表

由於FAT16是的FAT表項是2個字節,所以 FAT[3]是0x04, 也就是說下一個文件簇的位置是4, 完整的查找過程以下:

  • FAT[4] == 5
  • FAT[5] == 6
  • ...
  • FAT[11] == 12(0c)
  • FAT[12] == 0xFFFF (文件結束符)。

能夠看出 FAT就是一個連接,當前的值指向下一個簇號,直到以0xFFFF做爲結束。
同理讀取目錄也是同樣的,本例中,HOME目錄的起始地址是: 0x9200 + (0x0d - 2) * 2048 = 0xEA00
0.png

0xea00的內容同根目錄同樣,就是該目錄下的目錄表,若是該目錄的內容超過一個簇,也一樣去經過FAT表去查找其它的內容。

這就是FAT文件系統的基本概念和工做流程。不難看出FAT採用的這種鏈表結構,會致使碎片化會很嚴重,使用時間長了之後,一個文件的簇鏈獲得處都是。另外,FAT尚未權限管理,病毒程序就如入無人之境,能夠隨意複製和破壞。FAT的好處是簡單靈活,文件的個數不固定,且佔用磁盤空間少。可是,畢竟適應不了目前大容量高性能的要求,微軟從FAT12打補丁到FAT32後,就推出了NTFS文件系統。

後記

到這裏,文件系統的一些基礎知識就介紹完了,那麼瞭解它有哪些實際的意義呢?

首先,像電影裏面的黑客,能夠直接面對文件系統的原始數據,好比:從文件系統損壞的硬盤裏面恢復一些關鍵文件--經過讀取磁盤文件系統的元數據,好比說直接找目錄表,看看哪些文件還能夠被識別,再查找相應的Inode節點(或FAT表),讀取它的BLOCKS(或簇),只要物理上還沒損壞,就能恢復出來。

其次,能夠作一些磁盤類的工具,像文件搜索,恢復刪除文件之類--文件被刪除後,文件系統通常都是修改了些標誌位,如:bitmap的空閒標誌置1,表示空間被釋放等,其實文件的內容還在硬盤上,只要及時地找到文件的位置,就有恢復的可能。我曾經用python作過一個小工具,能夠瀏覽,提取iso 9600文件系統(光盤)裏的文件,這樣能夠在不使用虛擬光驅的狀況下,把iso鏡像文件的內容都提取出來。


歡迎你們來個人網站交流:般若程序蟬
qrcode_258.jpg

相關文章
相關標籤/搜索