欧美色欧美亚洲高清在线观看,国产特黄特色a级在线视频,国产一区视频一区欧美,亚洲成a 人在线观看中文

  1. <ul id="fwlom"></ul>

    <object id="fwlom"></object>

    <span id="fwlom"></span><dfn id="fwlom"></dfn>

      <object id="fwlom"></object>

      C語言書籍推薦(范文模版)

      時間:2019-05-12 21:03:43下載本文作者:會員上傳
      簡介:寫寫幫文庫小編為你整理了多篇相關(guān)的《C語言書籍推薦(范文模版)》,但愿對你工作學(xué)習(xí)有幫助,當然你在寫寫幫文庫還可以找到更多《C語言書籍推薦(范文模版)》。

      第一篇:C語言書籍推薦(范文模版)

      一、入門級書籍推薦

      1.C Programming:A Modern Approach(C語言程序設(shè)計:現(xiàn)代方法)

      簡介:《C語言程序設(shè)計:現(xiàn)代方法》是C語言的經(jīng)典之作,被譽為“近10年來最好的一部C語言著作”。書中 討論了標準C和C標準庫的全部特性,包括信號、setjmp/longjmp和可變參數(shù)列表等其他書中很少涉及的內(nèi)容。全 書由易而難、循序漸進、螺旋式地講述C語言,很好地處理了指針和位運算等難點。第2版覆蓋了C99標準,并提供 了對所有C99庫函數(shù)的參考,還擴展了GCC的內(nèi)容,增加了對抽象數(shù)據(jù)類型的討論,并針對新CPU和操作系統(tǒng)做了更 新?!禖語言程序設(shè)計:現(xiàn)代方法》尤為強調(diào)軟件工程和現(xiàn)代編程理念,在知識的闡述中突出工業(yè)界的最佳實踐、實際經(jīng)驗和編程風(fēng)格,使讀者能夠合理運用所學(xué),編寫出可讀性好、可靠性高和容易維護的代碼。書中精心選擇 了近500道習(xí)題,貼近實戰(zhàn),與敘述文字相得益彰。

      目前《C語言程序設(shè)計:現(xiàn)代方法》已被全球200多所學(xué)校采用為教材,包括哈佛大學(xué)、麻省理工學(xué)院、斯坦福 大學(xué)、加州大學(xué)伯克利分校、耶魯大學(xué)、加州理工學(xué)院等諸多名校。C語言程序設(shè)計

      2.C Primer plus 5th 內(nèi)容提要:

      本書全面講述了C語言編程的相關(guān)概念和知識。

      全書共17章。第1、2章學(xué)習(xí)C語言編程所需的預(yù)備知識。第3到15章介紹了C語言的相關(guān)知識,包括數(shù)據(jù)類型、格式化輸入輸出、運算符、表達式、流程控制語句、函數(shù)、數(shù)組和指針、字符串操作、內(nèi)存管理、位操作等等,知識內(nèi)容都針對C99標準;另外,第10章強化了對指針的討論,第12章引入了動態(tài)內(nèi)存分配的概念,這些內(nèi)容更加適合讀者的需求。第16章和第17章討論了C預(yù)處理器和C庫函數(shù)、高級數(shù)據(jù)表示(數(shù)據(jù)結(jié)構(gòu))方面的內(nèi)容。附錄給出了各章后面復(fù)習(xí)題、編程練習(xí)的答案和豐富的C編程參考資料。

      本書適合希望系統(tǒng)學(xué)習(xí)C語言的讀者,也適用于精通其他編程語言并希望進一步掌握和鞏固C編程技術(shù)的程序員。作者簡介:

      Stephen Prata在加利福尼亞州的Kentfield的Marin學(xué)院教授天文學(xué)、物理學(xué)和程序設(shè)計課程。他在加州工業(yè)學(xué)院獲得學(xué)士學(xué)位,從加州大學(xué)伯克利分校獲得博士學(xué)位。他最早接觸計算機,始于對星河的計算機建模。Stephen已經(jīng)編寫或與他人合作編寫了十多本書。其中包括C++Primer Plus和Unix Prinmer Plus.3.譚浩強 第三版

      國內(nèi)非常普及的學(xué)語言入門書籍,很多學(xué)校都是用它作為教材,對于C語言入門來說比較通俗易懂。不過書中有些地方不夠嚴謹,需要注意一些問題。下載地址:(百度文庫)

      http://wenku.baidu.com/view/ccc7cd868762caaedd33d455.html 二.提高級書籍推薦

      1.The C Programming Language Second Edition

      by Brian W.Kernighan, Dennis M.Ritchie.Prentice Hall PTR

      C程序設(shè)計語言 第2版·新版

      本書是由C語言的設(shè)計者Brian W.Kernighan和Dennis M.Ritchie編寫的一部介紹標準C語言及其程序設(shè)計方法的權(quán)威性經(jīng)典著作。全面、系統(tǒng)地講述了C語言的各個特性及程序設(shè)計的基本方法,包括基本概念、類型和表達式、控制流、函數(shù)與程序結(jié)構(gòu)、指針與數(shù)組、結(jié)構(gòu)、輸入與輸出、UNIX系統(tǒng)接口、標準庫等內(nèi)容。

      本書的講述深入淺出,配合典型例證,通俗易懂,實用性強,適合作為大專院校計算機專業(yè)或非計算機專業(yè)的C語言教材,也可以作為從事計算機相關(guān)軟硬件開發(fā)的技術(shù)人員的參考書。

      在計算機發(fā)展的歷史上,沒有哪一種程序設(shè)計語言像C語言這樣應(yīng)用如此廣泛。本書原著 即為C語言的設(shè)計者之一Dennis M.Ritchie和著名的計算機科學(xué)家Brian W.Kernighan合著的 一本介紹C語言的權(quán)威經(jīng)典著作。我們現(xiàn)在見到的大量論述C語言程序設(shè)計的教材和專著均以 此書為藍本。原著第1版中介紹的C語言成為后來廣泛使用的C語言版本—— 標準C的基礎(chǔ)。人們熟知的“hell, World”程序就是由本書首次引入的,現(xiàn)在,這一程序已經(jīng)成為所有程序設(shè) 計語言入門的第一課。原著第2版根據(jù)1987年制定的ANSIC標準做了適當?shù)男抻啠肓俗钚碌恼Z言形式,并增加了新的示例,通過簡潔的描述、典型的示例,作者全面、系統(tǒng)、準確地講述了C語言的各 個特性以及程序設(shè)計的基本方法。對于計算機從業(yè)人員來說,本書是一本必讀的程序設(shè)計語 言方面的參考書。

      下載地址:http://

      3.C Traps and Pitfalls(C陷阱與缺陷)

      作者以自己1985年在Bell實驗室時發(fā)表的一篇論文為基礎(chǔ),結(jié)合自己的工作經(jīng)驗擴展成為這本對C程序員具有珍貴價值的經(jīng)典著作。寫作本書的出發(fā)點不是要批判C語言,而是要幫助C程序員繞過編程過程中的陷阱和障礙。

      全書分為8章,分別從詞法分析、語法語義、連接、庫函數(shù)、預(yù)處理器、可移植性缺陷等幾個方面分析了C編程中可能遇到的問題。最后,作者用一章的篇幅給出了若干具有實用價值的建議。

      本書適合有一定經(jīng)驗的C程序員閱讀學(xué)習(xí),即便你是C編程高手,本書也應(yīng)該成為你的案頭必備書籍。

      《C陷阱與缺陷》Andrew Koenig Andrew Koenig的成名作,能幫助初學(xué)者減少90%的錯誤。一些錯誤(比如賦值符、運算符優(yōu)先級等)還是不時的會在自己的程序里出現(xiàn),只是已經(jīng)打過預(yù)防針了,很容易發(fā)現(xiàn)。

      下載地址:http://?from=like

      5.C Interfaces and Implementations(C語言接口與實現(xiàn))

      本書概念清晰、內(nèi)容新穎、實例詳盡,是一本有關(guān)設(shè)計、實現(xiàn)和有效使用C語言庫函數(shù),掌握創(chuàng)建可重用C語言軟件模塊技術(shù)的參考指南。本書倡導(dǎo)基于接口的C語言設(shè)計理念及其實現(xiàn)技術(shù),深入詳細地描述了24個C語言接口及其實現(xiàn)。本書通過敘述如何用一種與語言無關(guān)的方法將接口的設(shè)計與實現(xiàn)獨立開來,從而形成一種基于接口的設(shè)計途徑來創(chuàng)建可重用的API。本書是一本針對C語言程序員的不可多得的好書,也是值得所有希望掌握可重用軟件模塊技術(shù)的讀者閱讀的參考書籍。

      下載地址:(百度文庫)http://wenku.baidu.com/view/4573cf2d2af90242a895e57b.html

      第二篇:c語言書籍總結(jié)

      關(guān)于這個方向,我認為大概分3個階段:

      1、嵌入式linux上層應(yīng)用,包括QT的GUI開發(fā)

      2、嵌入式linux系統(tǒng)開發(fā)

      3、嵌入式linux驅(qū)動開發(fā)

      嵌入式目前主要面向的幾個操作系統(tǒng)是,LINUX,WINCE、VxWorks等等

      Linux是開源免費的,而且其源代碼是開放的,更加適合我們學(xué)習(xí)嵌入式。

      所以你可以嘗試以下路線:

      (1)C語言是所有編程語言中的強者,單片機、DSP、類似ARM的種種芯片的編程都可以用C語言搞定),因此必須非常熟練的掌握。

      推薦書籍:《The C Programming Language》 這本經(jīng)典的教材是老外寫的,也有中譯版本。

      (2)操作系統(tǒng)原理,是必需的,如果你是計算機專業(yè)畢業(yè)那也就無所謂了,如果是非計算機專業(yè)的就必須找一本比較淺顯的計算機原理書籍看一看,把啥叫“進程”“線程”“系統(tǒng)調(diào)度”等等基本問題搞清楚。

      (3)Linux操作系統(tǒng)就是用C語言編寫的,所以你也應(yīng)該先學(xué)習(xí)下Linux方面的編程,只有你會應(yīng)用了,才能近一步去了解其內(nèi)核的精髓。

      推薦書籍:《UNIX環(huán)境高級編程》(第2版)

      (4)了解ARM的架構(gòu),原理,以及其匯編指令,我們在嵌入式開發(fā)中,一般很少去寫匯編,但是最起碼的要求是能夠看懂a(chǎn)rm匯編。

      (5)系統(tǒng)移植的時候,就需要你從最下層的bootloader開始,然后內(nèi)核移植,文件系統(tǒng)移植等。而移植這部分對硬件的依賴是非常大的,其配置步驟也相對復(fù)雜,也沒有太多詳細資料。

      (6)驅(qū)動開發(fā)

      linux驅(qū)動程序設(shè)計既是個極富有挑戰(zhàn)性的領(lǐng)域,又是一個博大精深的內(nèi)容。

      linux驅(qū)動程序設(shè)計本質(zhì)是屬于linux內(nèi)核編程范疇的,因而是對linux內(nèi)核和內(nèi)核編程是有要求的。在學(xué)習(xí)前你要想了解linux內(nèi)核的組成,因為每一部分要詳細研究的話足夠可以擴展成一本厚書。

      以上只不過是大概的框架,在實際的開發(fā)中還會涉及很多東西,比如:交叉編譯、makefile、shell腳本等等,所以說學(xué)習(xí)嵌入式的周期較長,門檻較高,自學(xué)的話更是需要較強的學(xué)習(xí)能力和專業(yè)功底。只要能堅持下來一定會取得成功!

      其實LZ可以到一些嵌入式培訓(xùn)機構(gòu)的網(wǎng)站上看一下他們的課程設(shè)置,就會在腦子里有個清晰的思路,比如華清遠見的嵌入式linux課程設(shè)置就很專業(yè),華清遠見網(wǎng)站上的嵌入式內(nèi)容很豐富,嵌入式方面的信息更新也很迅速,沒事可以去轉(zhuǎn)轉(zhuǎn)

      第三篇:linux和C語言經(jīng)典書籍

      思想篇

      《Linux/Unix設(shè)計思想》

      圖書將Unix與Linux的原理有效地結(jié)合起來,總結(jié)了Unix/Linux軟件開發(fā)中的原則。在保留了第1版中Unix方面的內(nèi)容的同時,強調(diào)了Linux和開源領(lǐng)域的新思想。

      入門篇

      《Linux程序設(shè)計(第4版)》

      《Linux程序設(shè)計》是Linux程序設(shè)計領(lǐng)域的經(jīng)典名著,以簡單易懂、內(nèi)容全面和示例豐富而受到廣泛好評。中文版前兩版出版后,在國內(nèi)的 Linux愛好者和程序員中也引起了強烈反響,這一熱潮一直持續(xù)至今?!禠inux程序設(shè)計(第4版)》內(nèi)容組織更加嚴謹,譯者更是細心雕琢,保留了這部 權(quán)威著作的原汁原味。對Linux所提供的功能全面而準確的闡述,以及貫穿全書的示例程序體驗,使本書不僅成為初學(xué)者的最佳Linux程序設(shè)計指南,而且是中高級程序員不可或 缺的參考書。

      進程篇

      《理解Unix進程》

      本書是唯一一本專為現(xiàn)代web開發(fā)人員準備的Unix編程書。書中所有的例子都是用Ruby寫成,適用于所有具備高級語言經(jīng)驗的程序開發(fā)人員。書的重點內(nèi)容如下:

      1.文件描述符及其運作機制 2.何時才需要守護進程

      3.如何用fork(2)創(chuàng)建新進程 4.退出進程的4種不同的方式

      5.對于生成shell命令的實際考量以及如何避免這種情況 6.從高級層面上討論了創(chuàng)建進程所帶來的開銷及陷阱 7.Resque和Unicorn的內(nèi)部工作原理

      內(nèi)核篇

      《深入Linux內(nèi)核架構(gòu)》

      《深入Linux內(nèi)核架構(gòu)》是非常值得Linux程序員閱讀的圖書,堪稱是Linux內(nèi)核的詳解。

      書中討論了Linux 內(nèi)核的概念、結(jié)構(gòu)和實現(xiàn)。主要內(nèi)容包括多任務(wù)、調(diào)度和進程管理,物理內(nèi)存的管理以及內(nèi)核與相關(guān)硬件的交互,用戶空間的進程如何訪問虛擬內(nèi)存,如何編寫設(shè)備 驅(qū)動程序,模塊機制以及虛擬文件系統(tǒng),Ext 文件系統(tǒng)屬性和訪問控制表的實現(xiàn)方式,內(nèi)核中網(wǎng)絡(luò)的實現(xiàn),系統(tǒng)調(diào)用的實現(xiàn)方式,內(nèi)核對時間相關(guān)功能的處理,頁面回收和頁交換的相關(guān)機制以及審計的實現(xiàn)等。此外,本書借助內(nèi)核源代碼中最關(guān)鍵的部分進行講解,幫助讀者掌握重要的知識點,從而在運用中充分展現(xiàn)Linux 系統(tǒng)的魅力。

      shell篇

      《Linux Shell腳本攻略》(第2版版權(quán)已確定)

      《Linux Shell腳本攻略》 是Linux Shell 編程的實戰(zhàn)秘籍,程序員的獨門攻略:準備,動手,一舉成功!

      對新手而言,本書的內(nèi)容由淺入深且緊貼實踐,使得他們能夠快速地學(xué)以致用,而專業(yè)人士也能從本書中發(fā)現(xiàn)一些新鮮的東西,使自己的技巧更加純熟。

      《Linux命令行與Shell腳本編程大全(第2版)》 圖書堪稱黑客進階必讀,讓你輕松全面掌握命令行和shell

      全書分為四部分:第一部分介紹Linuxshell 命令行;第二部分介紹shell 腳本編程基礎(chǔ);第三部分深入探討shell 腳本編程的高級內(nèi)容;第四部分介紹如何在現(xiàn)實環(huán)境中使用shell 腳本。本書不僅涵蓋了詳盡的動手教程和現(xiàn)實世界中的實用信息,還提供了與所學(xué)內(nèi)容相關(guān)的參考信息和背景資料。本書內(nèi)容全面,語言簡練,示例豐富,適合于Linux 系統(tǒng)管理員及Linux 愛好者閱讀參考。

      應(yīng)用 編程

      不用說了肯定是《UNIX環(huán)境高級編程(第2版)》被稱為unix編程的圣經(jīng)。本書內(nèi)容權(quán)威,概念清晰,闡述精辟,對于所有層次UNIX程序員都是一本不可或缺的參考書。

      還有《UNIX網(wǎng)絡(luò)編程》可當字典來查閱。

      TCP/IP篇 《TCP/IP詳解》卷1、2、3作者W.Richard Stevens也是《unix環(huán)境高級編程》的作者,牛人出的書沒有一本不是經(jīng)典的。但是英年早逝,默哀一下。

      c語言

      推薦兩本日本圖書 《明解C語言》

      榮獲日本工學(xué)教育協(xié)會著作獎,是日本C語言入門第一書,暢銷20余萬冊。這本《明解C語言》講的都是很基本的東西,高樓平地起,他強調(diào)著最最基礎(chǔ)、同時也是最最重要的東西。書中不僅圖文并茂且示例豐富,設(shè)有190段代碼 和164幅圖表,對C語言的基礎(chǔ)知識進行了徹底剖析,內(nèi)容涉及數(shù)組、函數(shù)、指針、文件操作等。對于C語言語法以及一些難以理解的概念,均以精心繪制的示意 圖,清晰、通俗地進行講解。

      《征服C指針》

      被稱為日本最有營養(yǎng)的C參考書。作者是日本著名的“毒舌程序員”,其言辭犀利,觀點鮮明,往往能讓讀者迅速領(lǐng)悟要領(lǐng)。

      書中結(jié)合了作者多年的編程經(jīng)驗和感悟,從C語言指針的概念講起,通過實驗一步一步地為我們解釋了指針和數(shù)組、內(nèi)存、數(shù)據(jù)結(jié)構(gòu)的關(guān)系,展現(xiàn)了指針的常 見用法,揭示了各種使用技巧。另外,還通過獨特的方式教會我們怎樣解讀C語言那些讓人“糾結(jié)”的聲明語法,如何繞過C指針的陷阱。

      鳥哥的LINUX私房菜也是非常好的入門書籍

      C 語言”部分還有即將出版的《Head First C 中文版》

      第四篇:學(xué)習(xí)C語言的經(jīng)典書籍

      PART 1.推薦經(jīng)典書籍(內(nèi)容不全,慢慢補充)

      ①C語言:(讀完之后請混CSDN論壇進行鞏固)

      《C語言程序設(shè)計》

      作者:郭有強 編

      出 版 社:清華大學(xué)出版社

      評價:書很利索,該有的都有,如果你還沒有一本滿意的C語言課本,買它沒錯。(也可以閱讀外國的經(jīng)典C語言書籍)

      《C和指針》

      POINTERS ON C Kenneth A.Reek、徐波 人民郵電出版社

      評價:不算厚的書,糾正對指針的錯誤理解,這是必讀經(jīng)典,相信會帶給你很多思考。(單單一本還不夠,繼續(xù)往后看)

      《C陷阱與缺陷》

      Andrew Koenig、高巍

      人民郵電出版社

      評價:172頁,應(yīng)當1-2天看完,讀完豁然開朗,對C語言常見的陷阱進行剖析,必須經(jīng)典。

      《C專家編程》

      Expert C Programming Deep C Secrets Peter Van

      Der Linden 人民郵電出版社(2008-02出版)

      評價:200多頁,應(yīng)當2天左右看完,本書讀起來很舒坦,不可多得的好書,帶你領(lǐng)略語法之外的奧秘,必讀經(jīng)典。

      ②數(shù)據(jù)結(jié)構(gòu)與算法:

      《清華大學(xué)計算機系列教材?數(shù)據(jù)結(jié)構(gòu)(C語言版)(附光盤1張)》

      吳偉民、嚴蔚敏

      清華大學(xué)出版社

      評價:數(shù)據(jù)結(jié)構(gòu)都是類C的偽代碼描述,初次接觸編程的同學(xué)可能理解不了,我也一樣。我是看嚴蔚敏視頻學(xué)會的數(shù)據(jù)結(jié)構(gòu),希望這個辦法對你們也同樣有效。

      《計算機算法設(shè)計與分析(第3版)》

      王曉東

      電子工業(yè)出版社

      評價:這是我們的課本,不過的確是學(xué)習(xí)它才把數(shù)據(jù)結(jié)構(gòu)和算法入門了,為后面進階做了鋪墊。

      《算法藝術(shù)與信息學(xué)競賽?算法競賽入門經(jīng)典》

      劉汝佳

      清華大學(xué)出版社

      評價:了解數(shù)據(jù)結(jié)構(gòu)怎么用,常用算法與思想,書不厚,但很給力。初學(xué)可能有障礙,但這的確是入門經(jīng)典書籍,請不要放棄。

      《算法導(dǎo)論(原書第2版)》

      科曼(Cormen T.H.)、等、潘金貴

      機械工業(yè)出版社(2006-09出版)

      評價:經(jīng)典中的經(jīng)典,無需多說,不要問我看哪些內(nèi)容,負責(zé)任的說:有能力就多看點,其實都能看懂,書中的內(nèi)容與思想將會終身受用。

      《編程之美:微軟技術(shù)面試心得 》

      《編程之美》小組

      電子工業(yè)出版社

      評價:微軟出品,像是一本小故事集,將數(shù)學(xué)和編程之美展露無疑,學(xué)起來很輕松,反復(fù)回顧收獲頗豐。

      《編程珠璣(第2版)》

      Jon Bentley、黃倩、錢麗艷

      人民郵電出版社

      評價:主要是數(shù)據(jù)結(jié)構(gòu)和算法,都是常用的內(nèi)容,不過作者思維新奇,跟隨作者一起思考會碰撞出不少火花,評價非常高的一本書,本人沒有順序閱讀,感興趣的內(nèi)容翻了翻。

      《算法藝術(shù)與信息學(xué)競賽 》

      劉汝佳

      清華大學(xué)出版社

      評價:放在最后,因為此書的確很專業(yè),我這點智商也駕馭不了,信息學(xué)競賽必備。

      《程序員面試寶典(第3版)》

      歐立奇、劉洋、段韜

      電子工業(yè)出版社

      評價:這本書很神奇,大二下學(xué)期的時候我就買了并看完了,等到大三找工作的時候發(fā)現(xiàn)里邊的提到的陷阱題目還是有點意思的,C語言掌握的怎么樣拿這本書驗一驗就知道了。

      《數(shù)學(xué)之美》

      (《浪潮之巔》作者吳軍最新力作,李開復(fù)作序推薦,Google黑板報百萬點擊)

      評價:吳軍博士那是相當犀利,《浪潮之巔》足以讓人拜服了,此書一出無與爭鋒,放在數(shù)據(jù)結(jié)構(gòu)與算法這里比較合適。

      第五篇:[電腦書籍]語言簡介與入門

      我不想夸大或者貶低匯編語言。但我想說,匯編語言改變了20世紀的歷史。與前輩相比,我們這一代編程人員足夠的幸福,因為我們有各式各樣的編程語言,我們可以操作鍵盤、坐在顯示器面前,甚至使用鼠標、語音識別。我們可以使用鍵盤、鼠標來駕馭“個人計算機”,而不是和一群人共享一臺使用笨重的繼電器、開關(guān)去操作的巨型機。相比之下,我們的前輩不得不使用機器語言編寫程序,他們甚至沒有最簡單的匯編程序來把助記符翻譯成機器語言,而我們可以從上千種計算機語言中選擇我們喜歡的一種,而匯編,雖然不是一種“常用”的具有“快速原型開發(fā)”能力的語言,卻也是我們可以選擇的語言中的一種。

      每種計算機都有自己的匯編語言——沒必要指望匯編語言的可移植性,選擇匯編,意味著選擇性能而不是可移植或便于調(diào)試。這份文檔中講述的是x86匯編語言,此后的“匯編語言”一詞,如果不明示則表示ia32上的x86匯編語言。

      匯編語言是一種易學(xué),卻很難精通的語言?;叵氘斈辏覐某鯇W(xué)匯編到寫出第一個可運行的程序,只用了不到4個小時;然而直到今天,我仍然不敢說自己精通它。編寫快速、高效、并且能夠讓處理器“很舒服地執(zhí)行”的程序是一件很困難的事情,如果利用業(yè)余時間學(xué)習(xí),通常需要2-3年的時間才能做到。這份教材并不期待能夠教給你大量的匯編語言技巧。對于讀者來說,x86匯編語言“就在這里”。然而,不要僵化地局限于這份教材講述的內(nèi)容,因為它只能告訴你匯編語言是“這樣一回事”。學(xué)好匯編語言,更多的要靠一個人的創(chuàng)造力于悟性,我可以告訴你我所知道的技巧,但肯定這是不夠的。一位對我的編程生涯產(chǎn)生過重要影響的人曾經(jīng)對我說過這么一句話:

      寫匯編語言程序不是匯編語言最難的部分,創(chuàng)新才是。

      我想,愿意看這份文檔的人恐怕不會問我“為什么要學(xué)習(xí)匯編語言”這樣的問題;不過,我還是想說幾句:首先,匯編語言非常有用,我個人主張把它作為C語言的先修課程,因為通過學(xué)習(xí)匯編語言,你可以了解到如何有效地設(shè)計數(shù)據(jù)結(jié)構(gòu),讓計算機處理得更快,并使用更少的存儲空間;同時,學(xué)習(xí)匯編語言可以讓你熟悉計算機內(nèi)部運行機制,并且,有效地提高調(diào)試能力。就我個人的經(jīng)驗而言,調(diào)試一個非結(jié)構(gòu)化的程序的困難程度,要比調(diào)試一個結(jié)構(gòu)化的程序的難度高很多,因為“結(jié)構(gòu)化”是以犧牲運行效率來提高可讀性與可調(diào)試性,這對于完成一般軟件工程的編碼階段是非常必要的。然而,在一些地方,比如,硬件驅(qū)動程序、操作系統(tǒng)底層,或者程序中經(jīng)常需要執(zhí)行的代碼,結(jié)構(gòu)化程序設(shè)計的這些優(yōu)點有時就會被它的低效率所抹煞。另外,如果你想真正地控制自己的程序,只知道源代碼級的調(diào)試是遠遠不夠的。

      浮躁的人喜歡說,用C++寫程序足夠了,甚至說,他不僅僅掌握C++,而且精通STL、MFC。我不贊成這個觀點,掌握上面的那些是每一個編程人員都應(yīng)該做到的,然而C++只是我們“常用”的一種語言,它不是編程的全部。低層次的開發(fā)者喜歡說,嘿,C++是多么的強大,它可以做任何事情——這不是事實。便于維護、調(diào)試,這些確實是我們的追求目標,但是,寫程序不能僅僅追求這個目標,因為我們最終的目的是滿足設(shè)計需求,而不是個人非理性的理想。這份教材適合已經(jīng)學(xué)習(xí)過某種結(jié)構(gòu)化程序設(shè)計語言的讀者。其內(nèi)容基于我在1995年給別人講述匯編語言時所寫的講義。當然,如大家所希望的,它包含了最新的處理器所支持的特性,以及相應(yīng)的內(nèi)容。我假定讀者已經(jīng)知道了程序設(shè)計的一些基本概念,因為沒有這些是無法理解匯編語言程序設(shè)計的;此外,我希望讀者已經(jīng)有了比較良好的程序設(shè)計基礎(chǔ),因為如果你缺乏對于結(jié)構(gòu)化程序設(shè)計的認識,編寫匯編語言程序很可能很快就破壞了你的結(jié)構(gòu)化編程習(xí)慣,大大降低程序的可讀性、可維護性,最終讓你的程序陷于不得不廢棄的代碼堆之中?;旧?,這份文檔撰寫的目標是盡可能地便于自學(xué)。不過,它對你也有一些要求,盡管不是很高,但我還是強調(diào)一下。學(xué)習(xí)匯編語言,你需要

      膽量。不要害怕去接觸那些計算機的內(nèi)部工作機制。知識。了解計算機常用的數(shù)制,特別是二進制、十六進制、八進制,以及計算機保存數(shù)據(jù)的方法。

      開放。接受匯編語言與高級語言的差異,而不是去指責(zé)它如何的不好讀。經(jīng)驗。要求你擁有任意其他編程語言的一點點編程經(jīng)驗。頭腦。

      祝您編程愉快!

      先說一點和實際編程關(guān)系不太大的東西。當然,如果你迫切的想看到更實質(zhì)的內(nèi)容,完全可以先跳過這一章。

      那么,我想可能有一個問題對于初學(xué)匯編的人來說非常重要,那就是: 匯編語言到底是什么?

      匯編語言是一種最接近計算機核心的編碼語言。不同于任何高級語言,匯編語言幾乎可以完全和機器語言一一對應(yīng)。不錯,我們可以用機器語言寫程序,但現(xiàn)在除了沒有匯編程序的那些電腦之外,直接用機器語言寫超過1000條以上指令的人大概只能算作那些被我們成為“圣人”的犧牲者一類了。畢竟,記憶一些短小的助記符、由機器去考慮那些瑣碎的配位過程和檢查錯誤,比記憶大量的隨計算機而改變的十六進制代碼、可能弄錯而沒有任何提示要強的多。熟練的匯編語言編碼員甚至可以直接從十六進制代碼中讀出匯編語言的大致意思。當然,我們有更好的工具——匯編器和反匯編器。

      簡單地說,匯編語言就是機器語言的一種可以被人讀懂的形式,只不過它更容易記憶。至于宏匯編,則是包含了宏支持的匯編語言,這可以讓你編程的時候更專注于程序本身,而不是忙于計算和重寫代碼。

      匯編語言除了機器語言之外最接近計算機硬件的編程語言。由于它如此的接近計算機硬件,因此,它可以最大限度地發(fā)揮計算機硬件的性能。用匯編語言編寫的程序的速度通常要比高級語言和C/C++快很多--幾倍,幾十倍,甚至成百上千倍。當然,解釋語言,如解釋型LISP,沒有采用JIT技術(shù)的Java虛機中運行的Java等等,其程序速度更無法與匯編語言程序同日而語。

      永遠不要忽視匯編語言的高速。實際的應(yīng)用系統(tǒng)中,我們往往會用匯編徹底重寫某些經(jīng)常調(diào)用的部分以期獲得更高的性能。應(yīng)用匯編也許不能提高你的程序的穩(wěn)定性,但至少,如果你非常小心的話,它也不會降低穩(wěn)定性;與此同時,它可以大大地提高程序的運行速度。我強烈建議所有的軟件產(chǎn)品在最后Release之前對整個代碼進行Profile,并適當?shù)赜脜R編取代部分高級語言代碼。至少,匯編語言的知識可以告訴你一些有用的東西,比如,你有多少個寄存器可以用。有時,手工的優(yōu)化比編譯器的優(yōu)化更為有效,而且,你可以完全控制程序的實際行為。

      我想我在羅嗦了??傊谖覀兘Y(jié)束這一章之前,我想說,不要在優(yōu)化的時候把希望完全寄托在編譯器上——現(xiàn)實一些,再好的編譯器也不可能總是產(chǎn)生最優(yōu)的代碼。

      當時我學(xué)過BASIC, Fortran和Pascal,寫的是一個

      對一個包含100個32bit整數(shù)的數(shù)組進行快速排序,并且輸出出來的小程序。實際上用匯編器寫出的機器碼與在調(diào)試器中用它附帶的匯編程序?qū)懗龅臋C器碼還是有一些細微差別的,前者更大,然而卻可能更高效,因為匯編器能夠?qū)⒋a放置到適合處理器的地方這句話假定兩個程序進行了同等程度的優(yōu)化,一個寫的不好的匯編程序和一個寫的很好的C程序相比,匯編程序不一定更快。

      中央處理器(CPU)在微機系統(tǒng)處于“領(lǐng)導(dǎo)核心”的地位。匯編語言被編譯成機器語言之后,將由處理器來執(zhí)行。那么,首先讓我們來了解一下處理器的主要作用,這將幫助你更好地駕馭它。

      典型的處理器的主要任務(wù)包括 從內(nèi)存中獲取機器語言指令,譯碼,執(zhí)行

      根據(jù)指令代碼管理它自己的寄存器

      根據(jù)指令或自己的的需要修改內(nèi)存的內(nèi)容 響應(yīng)其他硬件的中斷請求

      一般說來,處理器擁有對整個系統(tǒng)的所有總線的控制權(quán)。對于Intel平臺而言,處理器擁有對數(shù)據(jù)、內(nèi)存和控制總線的控制權(quán),根據(jù)指令控制整個計算機的運行。在以后的章節(jié)中,我們還將討論系統(tǒng)中同時存在多個處理器的情況。

      處理器中有一些寄存器,這些寄存器可以保存特定長度的數(shù)據(jù)。某些寄存器中保存的數(shù)據(jù)對于系統(tǒng)的運行有特殊的意義。

      新的處理器往往擁有更多、具有更大字長的寄存器,提供更靈活的取指、尋址方式。寄存器

      如前所述,處理器中有一些可以保存數(shù)據(jù)的地方被稱作寄存器。

      寄存器可以被裝入數(shù)據(jù),你也可以在不同的寄存器之間移動這些數(shù)據(jù),或者做類似的事情?;旧?,像四則運算、位運算等這些計算操作,都主要是針對寄存器進行的。

      首先讓我來介紹一下80386上最常用的4個通用寄存器。先瞧瞧下面的圖形,試著理解一下: 上圖中,數(shù)字表示的是位。我們可以看出,EAX是一個32-bit寄存器。同時,它的低16-bit又可以通過AX這個名字來訪問;AX又被分為高、低8bit兩部分,分別由AH和AL來表示。對于EAX、AX、AH、AL的改變同時也會影響與被修改的那些寄存器的值。從而事實上只存在一個32-bit的寄存器EAX,而它可以通過4種不同的途徑訪問。

      也許通過名字能夠更容易地理解這些寄存器之間的關(guān)系。EAX中的E的意思是“擴展的”,整個EAX的意思是擴展的AX。X的意思Intel沒有明示,我個人認為表示它是一個可變的量。而AH、AL中的H和L分別代表高和低。

      為什么要這么做呢?主要由于歷史原因。早期的計算機是8位的,8086是第一個16位處理器,其通用寄存器的名字是AX,BX等等;80386是Intel推出的第一款I(lǐng)A-32系列處理器,所有的寄存器都被擴充為32位。為了能夠兼容以前的16位應(yīng)用程序,80386不能將這些寄存器依舊命名為AX、BX,并且簡單地將他們擴充為32位——這將增加處理器在處理指令方面的成本。

      Intel微處理器的寄存器列表(在本章先只介紹80386的寄存器,MMX寄存器以及其他新一代處理器的新寄存器將在以后的章節(jié)介紹)通用寄存器

      下面介紹通用寄存器及其習(xí)慣用法。顧名思義,通用寄存器是那些你可以根據(jù)自己的意愿使用的寄存器,修改他們的值通常不會對計算機的運行造成很大的影響。通用寄存器最多的用途是計算。EAX 32-bit寬

      通用寄存器。相對其他寄存器,在進行運算方面比較常用。在保護模式中,也可以作為內(nèi)存偏移指針(此時,DS作為段 寄存器或選擇器)EBX 32-bit寬

      通用寄存器。通常作為內(nèi)存偏移指針使用(相對于EAX、ECX、EDX),DS是默認的段寄存器或選擇器。在保護模式中,同樣可以起這個作用。

      ECX 32-bit寬

      通用寄存器。通常用于特定指令的計數(shù)。在保護模式中,也可以作為內(nèi)存偏移指針(此時,DS作為 寄存器或段選擇器)。

      EDX 32-bit寬

      通用寄存器。在某些運算中作為EAX的溢出寄存器(例如乘、除)。在保護模式中,也可以作為內(nèi)存偏移指針(此時,DS作為段 寄存器或選擇器)。上述寄存器同EAX一樣包括對應(yīng)的16-bit和8-bit分組。用作內(nèi)存指針的特殊寄存器 ESI 32-bit寬

      通常在內(nèi)存操作指令中作為“源地址指針”使用。當然,ESI可以被裝入任意的數(shù)值,但通常沒有人把它當作通用寄存器來用。DS是默認段寄存器或選擇器。EDI 32-bit寬

      通常在內(nèi)存操作指令中作為“目的地址指針”使用。當然,EDI也可以被裝入任意的數(shù)值,但通常沒有人把它當作通用寄存器來用。DS是默認段寄存器或選擇器。EBP 32-bit寬

      這也是一個作為指針的寄存器。通常,它被高級語言編譯器用以建造‘堆棧幀’來保存函數(shù)或過程的局部變量,不過,還是那句話,你可以在其中保存你希望的任何數(shù)據(jù)。SS是它的默認段寄存器或選擇器。

      注意,這三個寄存器沒有對應(yīng)的8-bit分組。換言之,你可以通過SI、DI、BP作為別名訪問他們的低16位,卻沒有辦法直接訪問他們的低8位。段寄存器和選擇器

      實模式下的段寄存器到保護模式下?lián)u身一變就成了選擇器。不同的是,實模式下的“段寄存器”是16-bit的,而保護模式下的選擇器是32-bit的。

      CS 代碼段,或代碼選擇器。同IP寄存器(稍后介紹)一同指向當前正在執(zhí)行的那個地址。處理器執(zhí)行時從這個寄存器指向的段(實模式)或內(nèi)存(保護模式)中獲取指令。除了跳轉(zhuǎn)或其他分支指令之外,你無法修改這個寄存器的內(nèi)容。

      DS 數(shù)據(jù)段,或數(shù)據(jù)選擇器。這個寄存器的低16 bit連同ESI一同指向的指令將要處理的內(nèi)存。同時,所有的內(nèi)存操作指令 默認情況下都用它指定操作段(實模式)或內(nèi)存(作為選擇器,在保護模式。這個寄存器可以被裝入任意數(shù)值,然而在這么做的時候需要小心一些。方法是,首先把數(shù)據(jù)送給AX,然后再把它從AX傳送給DS(當然,也可以通過堆棧來做).ES 附加段,或附加選擇器。這個寄存器的低16 bit連同EDI一同指向的指令將要處理的內(nèi)存。同樣的,這個寄存器可以被裝入任意數(shù)值,方法和DS類似。

      FS F段或F選擇器(推測F可能是Free?)。可以用這個寄存器作為默認段寄存器或選擇器的一個替代品。它可以被裝入任何數(shù)值,方法和DS類似。

      GS G段或G選擇器(G的意義和F一樣,沒有在Intel的文檔中解釋)。它和FS幾乎完全一樣。SS 堆棧段或堆棧選擇器。這個寄存器的低16 bit連同ESP一同指向下一次堆棧操作(push和pop)所要使用的堆棧地址。這個寄存器也可以被裝入任意數(shù)值,你可以通過入棧和出棧操作來給他賦值,不過由于堆棧對于很多操作有很重要的意義,因此,不正確的修改有可能造成對堆棧的破壞。

      * 注意 一定不要在初學(xué)匯編的階段把這些寄存器弄混。他們非常重要,而一旦你掌握了他們,你就可以對他們做任意的操作了。段寄存器,或選擇器,在沒有指定的情況下都是使用默認的那個。這句話在現(xiàn)在看來可能有點稀里糊涂,不過你很快就會在后面知道如何去做。特殊寄存器(指向到特定段或內(nèi)存的偏移量):

      EIP 這個寄存器非常的重要。這是一個32位寬的寄存器,同CS一同指向即將執(zhí)行的那條指令的地址。不能夠直接修改這個寄存器的值,修改它的唯一方法是跳轉(zhuǎn)或分支指令。(CS是默認的段或選擇器)ESP 這個32位寄存器指向堆棧中即將被操作的那個地址。盡管可以修改它的值,然而并不提倡這樣做,因為如果你不是非常明白自己在做什么,那么你可能造成堆棧的破壞。對于絕大多數(shù)情況而言,這對程序是致命的。(SS是默認的段或選擇器)IP: Instruction Pointer, 指令指針 SP: Stack Pointer, 堆棧指針

      好了,上面是最基本的寄存器。下面是一些其他的寄存器,你甚至可能沒有聽說過它們。(都是32位寬):

      CR0, CR2, CR3(控制寄存器)。舉一個例子,CR0的作用是切換實模式和保護模式。還有其他一些寄存器,D0, D1, D2, D3, D6和D7(調(diào)試寄存器)。他們可以作為調(diào)試器的硬件支持來設(shè)置條件斷點。

      TR3, TR4, TR5, TR6 和 TR? 寄存器(測試寄存器)用于某些條件測試。

      最后我們要說的是一個在程序設(shè)計中起著非常關(guān)鍵的作用的寄存器:標志寄存器。本節(jié)中部份表格來自David Jurgens的HelpPC 2.10快速參考手冊。在此謹表謝意。先說一點和實際編程關(guān)系不太大的東西。當然,如果你迫切的想看到更實質(zhì)的內(nèi)容,完全可以先跳過這一章。

      那么,我想可能有一個問題對于初學(xué)匯編的人來說非常重要,那就是: 匯編語言到底是什么?

      匯編語言是一種最接近計算機核心的編碼語言。不同于任何高級語言,匯編語言幾乎可以完全和機器語言一一對應(yīng)。不錯,我們可以用機器語言寫程序,但現(xiàn)在除了沒有匯編程序的那些電腦之外,直接用機器語言寫超過1000條以上指令的人大概只能算作那些被我們成為“圣人”的犧牲者一類了。畢竟,記憶一些短小的助記符、由機器去考慮那些瑣碎的配位過程和檢查錯誤,比記憶大量的隨計算機而改變的十六進制代碼、可能弄錯而沒有任何提示要強的多。熟練的匯編語言編碼員甚至可以直接從十六進制代碼中讀出匯編語言的大致意思。當然,我們有更好的工具——匯編器和反匯編器。

      簡單地說,匯編語言就是機器語言的一種可以被人讀懂的形式,只不過它更容易記憶。至于宏匯編,則是包含了宏支持的匯編語言,這可以讓你編程的時候更專注于程序本身,而不是忙于計算和重寫代碼。

      匯編語言除了機器語言之外最接近計算機硬件的編程語言。由于它如此的接近計算機硬件,因此,它可以最大限度地發(fā)揮計算機硬件的性能。用匯編語言編寫的程序的速度通常要比高級語言和C/C++快很多--幾倍,幾十倍,甚至成百上千倍。當然,解釋語言,如解釋型LISP,沒有采用JIT技術(shù)的Java虛機中運行的Java等等,其程序速度更無法與匯編語言程序同日而語。

      永遠不要忽視匯編語言的高速。實際的應(yīng)用系統(tǒng)中,我們往往會用匯編徹底重寫某些經(jīng)常調(diào)用的部分以期獲得更高的性能。應(yīng)用匯編也許不能提高你的程序的穩(wěn)定性,但至少,如果你非常小心的話,它也不會降低穩(wěn)定性;與此同時,它可以大大地提高程序的運行速度。我強烈建議所有的軟件產(chǎn)品在最后Release之前對整個代碼進行Profile,并適當?shù)赜脜R編取代部分高級語言代碼。至少,匯編語言的知識可以告訴你一些有用的東西,比如,你有多少個寄存器可以用。有時,手工的優(yōu)化比編譯器的優(yōu)化更為有效,而且,你可以完全控制程序的實際行為。

      我想我在羅嗦了??傊?,在我們結(jié)束這一章之前,我想說,不要在優(yōu)化的時候把希望完全寄托在編譯器上——現(xiàn)實一些,再好的編譯器也不可能總是產(chǎn)生最優(yōu)的代碼。

      當時我學(xué)過BASIC, Fortran和Pascal,寫的是一個

      對一個包含100個32bit整數(shù)的數(shù)組進行快速排序,并且輸出出來的小程序。實際上用匯編器寫出的機器碼與在調(diào)試器中用它附帶的匯編程序?qū)懗龅臋C器碼還是有一些細微差別的,前者更大,然而卻可能更高效,因為匯編器能夠?qū)⒋a放置到適合處理器的地方這句話假定兩個程序進行了同等程度的優(yōu)化,一個寫的不好的匯編程序和一個寫的很好的C程序相比,匯編程序不一定更快。

      在前一節(jié)中的x86基本寄存器的介紹,對于一個匯編語言編程人員來說是不可或缺的?,F(xiàn)在你知道,寄存器是處理器內(nèi)部的一些保存數(shù)據(jù)的存儲單元。僅僅了解這些是不足以寫出一個可用的匯編語言程序的,但你已經(jīng)可以大致讀懂一般匯編語言程序了(不必驚訝,因為匯編語言的祝記符和英文單詞非常接近),因為你已經(jīng)了解了關(guān)于基本寄存器的絕大多數(shù)知識。在正式引入第一個匯編語言程序之前,我粗略地介紹一下匯編語言中不同進制整數(shù)的表示方法。如果你不了解十進制以外的其他進制,請把鼠標移動到這里。

      匯編語言中的整數(shù)常量表示

      十進制整數(shù)

      這是匯編器默認的數(shù)制。直接用我們熟悉的表示方式表示即可。例如,1234表示十進制的1234。不過,如果你指定了使用其他數(shù)制,或者有凡事都進行完整定義的小愛好,也可以寫成[十進制數(shù)]d或[十進制數(shù)]D的形式。十六進制數(shù)

      這是匯編程序中最常用的數(shù)制,我個人比較偏愛使用十六進制表示數(shù)據(jù),至于為什么,以后我會作說明。十六進制數(shù)表示為0[十六進制數(shù)]h或0[十六進制數(shù)]H,其中,如果十六進制數(shù)的第一位是數(shù)字,則開頭的0可以省略。例如,7fffh, 0ffffh,等等。二進制數(shù)

      這也是一種常用的數(shù)制。二進制數(shù)表示為[二進制數(shù)]b或[二進制數(shù)]B。一般程序中用二進制數(shù)表示掩碼(mask code)等數(shù)據(jù)非常的直觀,但需要些很長的數(shù)據(jù)(4位二進制數(shù)相當于一位十六進制數(shù))。例如,1010110b。八進制數(shù)

      八進制數(shù)現(xiàn)在已經(jīng)不是很常用了(確實還在用,一個典型的例子是Unix的文件屬性)。八進制數(shù)的形式是[八進制數(shù)]q、[八進制數(shù)]Q、[八進制數(shù)]o、[八進制數(shù)]O。例如,777Q。需要說明的是,這些方法是針對宏匯編器(例如,MASM、TASM、NASM)說的,調(diào)試器默認使用十六進制表示整數(shù),并且不需要特別的聲明(例如,在調(diào)試器中直接用FFFF表示十進制的65535,用10表示十進制的16)。

      現(xiàn)在我們來寫一小段匯編程序,修改EAX、EBX、ECX、EDX的數(shù)值。我們假定程序執(zhí)行之前,寄存器中的數(shù)值是全0:

      ? X H L EAX 0000 00 00 EBX 0000 00 00 ECX 0000 00 00 EDX 0000 00 00

      正如前面提到的,EAX的高16bit是沒有辦法直接訪問的,而AX對應(yīng)它的低16bit,AH、AL分別對應(yīng)AX的高、低8bit。

      mov eax, 012345678h mov ebx, 0abcdeffeh mov ecx, 1 mov edx, 2;將012345678h送入eax;將0abcdeffeh送入ebx;將000000001h送入ecx;將000000002h送入edx

      則執(zhí)行上述程序段之后,寄存器的內(nèi)容變?yōu)椋?/p>

      ? X H L EAX 1234 56 78 EBX abcd ef fe ECX 0000 00 01 EDX 0000 00 02

      那么,你已經(jīng)了解了mov這個指令(mov是move的縮寫)的一種用法。它可以將數(shù)送到寄存器中。我們來看看下面的代碼:

      mov eax, ebx mov ecx, edx;ebx內(nèi)容送入eax;edx內(nèi)容送入ecx

      則寄存器內(nèi)容變?yōu)椋?/p>

      ? X H L EAX abcd ef fe EBX abcd ef fe ECX 0000 00 02 EDX 0000 00 02

      我們可以看到,“move”之后,數(shù)據(jù)依然保存在原來的寄存器中。不妨把mov指令理解為“送入”,或“裝入”。

      練習(xí)題

      把寄存器恢復(fù)成都為全0的狀態(tài),然后執(zhí)行下面的代碼:

      mov eax, 0a1234h mov bx, ax mov ah, bl mov al, bh;將0a1234h送入eax;將ax的內(nèi)容送入bx;將bl內(nèi)容送入ah;將bh內(nèi)容送入al

      思考:此時,EAX的內(nèi)容將是多少?[答案]

      下面我們將介紹一些指令。在介紹指令之前,我們約定:

      使用Intel文檔中的寄存器表示方式

      reg32 32-bit寄存器(表示EAX、EBX等)

      reg16 16-bit寄存器(在32位處理器中,這AX、BX等)reg8 8-bit寄存器(表示AL、BH等)imm32 32-bit立即數(shù)(可以理解為常數(shù))imm16 16-bit立即數(shù) imm8 8-bit立即數(shù)

      在寄存器中載入另一寄存器,或立即數(shù)的值:

      mov reg32,(reg32 | imm8 | imm16 | imm32)mov reg32,(reg16 | imm8 | imm16)mov reg8,(reg8 | imm8)

      例如,mov eax, 010h表示,在eax中載入00000010h。需要注意的是,如果你希望在寄存器中裝入0,則有一種更快的方法,在后面我們將提到。

      交換寄存器的內(nèi)容:

      xchg reg32, reg32 xchg reg16, reg16 xchg reg8, reg8

      例如,xchg ebx, ecx,則ebx與ecx的數(shù)值將被交換。由于系統(tǒng)提供了這個指令,因此,采用其他方法交換時,速度將會較慢,并需要占用更多的存儲空間,編程時要避免這種情況,即,盡量利用系統(tǒng)提供的指令,因為多數(shù)情況下,這意味著更小、更快的代碼,同時也杜絕了錯誤(如果說Intel的CPU在交換寄存器內(nèi)容的時候也會出錯,那么它就不用賣CPU了。而對于你來說,檢查一行代碼的正確性也顯然比檢查更多代碼的正確性要容易)剛才的習(xí)題的程序用下面的代碼將更有效: mov eax, 0a1234h mov bx, ax xchg ah, al;將0a1234h送入eax;將ax內(nèi)容送入bx;交換ah, al的內(nèi)容

      遞增或遞減寄存器的值:

      inc reg(8,16,32)dec reg(8,16,32)

      這兩個指令往往用于循環(huán)中對指針的操作。需要說明的是,某些時候我們有更好的方法來處理循環(huán),例如使用loop指令,或rep前綴。這些將在后面的章節(jié)中介紹。

      將寄存器的數(shù)值與另一寄存器,或立即數(shù)的值相加,并存回此寄存器:

      add reg32, reg32 / imm(8,16,32)add reg16, reg16 / imm(8,16)add reg8, reg8 / imm(8)

      例如,add eax, edx,將eax+edx的值存入eax。減法指令和加法類似,只是將add換成sub。

      需要說明的是,與高級語言不同,匯編語言中,如果要計算兩數(shù)之和(差、積、商,或一般地說,運算結(jié)果),那么必然有一個寄存器被用來保存結(jié)果。在PASCAL中,我們可以用nA := nB + nC來讓nA保存nB+nC的結(jié)果,然而,匯編語言并不提供這種方法。如果你希望保持寄存器中的結(jié)果,需要用另外的指令。這也從另一個側(cè)面反映了“寄存器”這個名字的意義。數(shù)據(jù)只是“寄存”在那里。如果你需要保存數(shù)據(jù),那么需要將它放到內(nèi)存或其他地方。

      類似的指令還有and、or、xor(與,或,異或)等等。它們進行的是邏輯運算。

      我們稱add、mov、sub、and等稱為為指令助記符(這么叫是因為它比機器語言容易記憶,而起作用就是方便人記憶,某些資料中也稱為指令、操作碼、opcode[operation code]等);后面的參數(shù)成為操作數(shù),一個指令可以沒有操作數(shù),也可以有一兩個操作數(shù),通常有一個操作數(shù)的指令,這個操作數(shù)就是它的操作對象;而兩個參數(shù)的指令,前一個操作數(shù)一般是保存操作結(jié)果的地方,而后一個是附加的參數(shù)。

      我不打算在這份教程中用大量的篇幅介紹指令——很多人做得比我更好,而且指令本身并不是重點,如果你學(xué)會了如何組織語句,那么只要稍加學(xué)習(xí)就能輕易掌握其他指令。更多的指令可以參考Intel提供的資料。編寫程序的時候,也可以參考一些在線參考手冊。Tech!Help和HelpPC 2.10盡管已經(jīng)很舊,但足以應(yīng)付絕大多數(shù)需要。

      聰明的讀者也許已經(jīng)發(fā)現(xiàn),使用sub eax, eax,或者xor eax, eax,可以得到與mov eax, 0類似的效果。在高級語言中,你大概不會選擇用a=a-a來給a賦值,因為測試會告訴你這么做更慢,簡直就是在自找麻煩,然而在匯編語言中,你會得到相反的結(jié)論,多數(shù)情況下,以由快到慢的速度排列,這三條指令將是xor eax, eax、sub eax, eax和mov eax, 0。

      為什么呢?處理器在執(zhí)行指令時,需要經(jīng)過幾個不同的階段:取指、譯碼、取數(shù)、執(zhí)行。

      我們反復(fù)強調(diào),寄存器是CPU的一部分。從寄存器取數(shù),其速度很顯然要比從內(nèi)存中取數(shù)快。那么,不難理解,xor eax, eax要比mov eax, 0更快一些。

      那么,為什么a=a-a通常要比a=0慢一些呢?這和編譯器的優(yōu)化有一定關(guān)系。多數(shù)編譯器會把a=a-a翻譯成類似下面的代碼(通常,高級語言通過ebp和偏移量來訪問局部變量;程序中,x為a相對于本地堆的偏移量,在只包含一個32-bit整形變量的程序中,這個值通常是4):

      mov eax, dword ptr [ebp-x] sub eax, dword ptr [ebp-x] mov dword ptr [ebp-x],eax

      而把a=0翻譯成

      mov dword ptr [ebp-x], 0

      上面的翻譯只是示意性的,略去了很多必要的步驟,如保護寄存器內(nèi)容、恢復(fù)等等。如果你對與編譯程序的實現(xiàn)過程感興趣,可以參考相應(yīng)的書籍。多數(shù)編譯器(特別是C/C++編譯器,如Microsoft Visual C++)都提供了從源代碼到宏匯編語言程序的附加編譯輸出選項。這種情況下,你可以很方便地了解編譯程序執(zhí)行的輸出結(jié)果;如果編譯程序沒有提供這樣的功能也沒有關(guān)系,調(diào)試器會讓你看到編譯器的編譯結(jié)果。

      如果你明確地知道編譯器編譯出的結(jié)果不是最優(yōu)的,那就可以著手用匯編語言來重寫那段代碼了。怎么確認是否應(yīng)該用匯編語言重寫呢?

      使用匯編語言重寫代碼之前需要確認的幾件事情

      首先,這種優(yōu)化最好有明顯的效果。比如,一段循環(huán)中的計算,等等。一條語句的執(zhí)行時間是很短的,現(xiàn)在新的CPU的指令周期都在0.000000001s以下,Intel甚至已經(jīng)做出了4GHz主頻(主頻的倒數(shù)是時鐘周期)的CPU,如果你的代碼自始至終只執(zhí)行一次,并且你只是減少了幾個時鐘周期的執(zhí)行時間,那么改變將是無法讓人察覺的;很多情況下,這種“優(yōu)化”并不被提倡,盡管它確實減少了執(zhí)行時間,但為此需要付出大量的時間、人力,多數(shù)情況下得不償失(極端情況,比如你的設(shè)備內(nèi)存價格非常昂貴的時候,這種優(yōu)化也許會有意義)。其次,確認你已經(jīng)使用了最好的算法,并且,你優(yōu)化的程序的實現(xiàn)是正確的。匯編語言能夠提供同樣算法的最快實現(xiàn),然而,它并不是萬金油,更不是解決一切的靈丹妙藥。用高級語言實現(xiàn)一種好的算法,不一定會比匯編語言實現(xiàn)一種差的算法更慢。不過需要注意的是,時間、空間復(fù)雜度最小的算法不一定就是解決某一特定問題的最佳算法。舉例說,快速排序在完全逆序的情況下等價于冒泡排序,這時其他方法就比它快。同時,用匯編語言優(yōu)化一個不正確的算法實現(xiàn),將給調(diào)試帶來很大的麻煩。

      最后,確認你已經(jīng)將高級語言編譯器的性能發(fā)揮到極致。Microsoft的編譯器在RELEASE模式和DEBUG模式會有差異相當大的輸出,而對于GNU系列的編譯器而言,不同級別的優(yōu)化也會生成幾乎完全不同的代碼。此外,在編程時對于問題的嚴格定義,可以極大地幫助編譯器的優(yōu)化過程。如何優(yōu)化高級語言代碼,使其編譯結(jié)果最優(yōu)超出了本教程的范圍,但如果你不能確認已經(jīng)發(fā)揮了編譯器的最大效能,用匯編語言往往是一種更為費力的方法。

      還有一點非常重要,那就是你明白自己做的是什么。好的高級語言編譯器有時會有一些讓人難以理解的行為,比如,重新排列指令順序,等等。如果你發(fā)現(xiàn)這種情況,那么優(yōu)化的時候就應(yīng)該小心——編譯器很可能比你擁有更多的關(guān)于處理器的知識,例如,對于一個超標量處理器,編譯器會對指令序列進行“封包”,使他們盡可能的并行執(zhí)行;此外,宏匯編器有時會自動插入一些nop指令,其作用是將指令湊成整數(shù)字長(32-bit,對于16-bit處理器,是16-bit)。這些都是提高代碼性能的必要措施,如果你不了解處理器,那么最好不要改動編譯器生成的代碼,因為這種情況下,盲目的修改往往不會得到預(yù)期的效果。

      曾經(jīng)在一份雜志上看到過有人用純機器語言編寫程序。不清楚到底這是不是編輯的失誤,因為一個頭腦正常的人恐怕不會這么做程序,即使它不長、也不復(fù)雜。首先,匯編器能夠完成某些封包操作,即使不行,也可以用db偽指令來寫指令;用匯編語言寫程序可以防止很多錯誤的發(fā)生,同時,它還減輕了人的負擔(dān),很顯然,“完全用機器語言寫程序”是完全沒有必要的,因為匯編語言可以做出完全一樣的事情,并且你可以依賴它,因為計算機不會出錯,而人總有出錯的時候。此外,如前面所言,如果用高級語言實現(xiàn)程序的代價不大(例如,這段代碼在程序的整個執(zhí)行過程中只執(zhí)行一遍,并且,這一遍的執(zhí)行時間也小于一秒),那么,為什么不用高級語言實現(xiàn)呢?

      一些比較狂熱的編程愛好者可能不太喜歡我的這種觀點。比方說,他們可能希望精益求精地優(yōu)化每一字節(jié)的代碼。但多數(shù)情況下我們有更重要的事情,例如,你的算法是最優(yōu)的嗎?你已經(jīng)把程序在高級語言許可的范圍內(nèi)優(yōu)化到盡頭了嗎?并不是所有的人都有資格這樣說。匯編語言是這樣一件東西,它足夠的強大,能夠控制計算機,完成它能夠?qū)崿F(xiàn)的任何功能;同時,因為它的強大,也會提高開發(fā)成本,并且,難于維護。因此,我個人的建議是,如果在軟件開發(fā)中使用匯編語言,則應(yīng)在軟件接近完成的時候使用,這樣可以減少很多不必要的投入。

      第二章中,我介紹了x86系列處理器的基本寄存器。這些寄存器對于x86兼容處理器仍然是有效的,如果你偏愛AMD的CPU,那么使用這些寄存器的程序同樣也可以正常運行。

      不過現(xiàn)在說用匯編語言進行優(yōu)化還為時尚早——不可能寫程序,而只操作這些寄存器,因為這樣只能完成非常簡單的操作,既然是簡單的操作,那可能就會讓人覺得乏味,甚至找一臺足夠快的機器窮舉它的所有結(jié)果(如果可以窮舉的話),并直接寫程序調(diào)用,因為這樣通常會更快。但話說回來,看完接下來的兩章——內(nèi)存和堆棧操作,你就可以獨立完成幾乎所有的任務(wù)了,配合第五章中斷、第六章子程序的知識,你將知道如何駕馭處理器,并讓它為你工作。

      數(shù)字計算機內(nèi)部只支持二進制數(shù),因為這樣計算機 只需要表示兩種(某些情況是3種,這一內(nèi)容超過了 這份教程的范圍,如果您感興趣,可以參考數(shù)字邏 輯電路的相關(guān)書籍)狀態(tài).對于電路而言,這表現(xiàn) 為高、低電平,或者開、關(guān),分別非常明顯,因而 工作比較穩(wěn)定;另一方面,由于只有兩種狀態(tài),設(shè) 計起來也比較簡單。這樣,使用二進制意味著低成 本、穩(wěn)定,多數(shù)情況下,這也意味著快速。

      與十進制類似,我們可以用下面的式子來換算出一 個任意形如am-1??a3a2a1a0 的m位r進制數(shù)對應(yīng)的 數(shù)值n:

      程序設(shè)計中常用十六進制和八進制數(shù)字代替二進制 數(shù),其原因在于,16和8是2的整次方冪,這樣,一 位十六或八進制數(shù)可以表示整數(shù)個二進制位。十六 進制中,使用字母A、B、C、D、E、F表示10-15,而十六進制或八進制數(shù)制表示的的數(shù)字比二進制數(shù) 更短一些。

      EAX的內(nèi)容為000A3412h.在前面的章節(jié)中,我們已經(jīng)了解了寄存器的基本使用方法。而正如結(jié)尾提到的那樣,僅僅使用寄存器做一點運算是沒有什么太大意義的,畢竟它們不能保存太多的數(shù)據(jù),因此,對編程人員而言,他肯定迫切地希望訪問內(nèi)存,以保存更多的數(shù)據(jù)。

      我將分別介紹如何在保護模式和實模式操作內(nèi)存,然而在此之前,我們先熟悉一下這兩種模式中內(nèi)存的結(jié)構(gòu)。3.1 實模式

      事實上,在實模式中,內(nèi)存比保護模式中的結(jié)構(gòu)更令人困惑。內(nèi)存被分割成段,并且,操作內(nèi)存時,需要指定段和偏移量。不過,理解這些概念是非常容易的事情。請看下面的圖:

      段寄存器這種格局是早期硬件電路限制留下的一個傷疤。地址總線在當時有20-bit。

      然而20-bit的地址不能放到16-bit的寄存器里,這意味著有4-bit必須放到別的地方。因此,為了訪問所有的內(nèi)存,必須使用兩個16-bit寄存器。

      這一設(shè)計上的折衷方案導(dǎo)致了今天的段-偏移量格局。最初的設(shè)計中,其中一個寄存器只有4-bit有效,然而為了簡化程序,兩個寄存器都是16-bit有效,并在執(zhí)行時求出加權(quán)和來標識20-bit地址。

      偏移量是16-bit的,因此,一個段是64KB。下面的圖可以幫助你理解20-bit地址是如何形成的:

      段-偏移量標識的地址通常記做 段:偏移量 的形式。

      由于這樣的結(jié)構(gòu),一個內(nèi)存有多個對應(yīng)的地址。例如,0000:0010和0001:0000指的是同一內(nèi)存地址。又如,0000:1234 = 0123:0004 = 0120:0034 = 0100:0234 0001:1234 = 0124:0004 = 0120:0044 = 0100:0244

      作為負面影響之一,在段上加1相當于在偏移量上加16,而不是一個“全新”的段。反之,在偏移量上加16也和在段上加1等價。某些時候,據(jù)此認為段的“粒度”是16字節(jié)。

      練習(xí)題

      嘗試一下將下面的地址轉(zhuǎn)化為20bit的地址:

      2EA8:D678 26CF:8D5F 453A:CFAD 2933:31A6 5924:DCCF 694E:175A 2B3C:D218 728F:6578 68E1:A7DC 57EC:AEEA

      稍高一些的要求是,寫一個程序?qū)⒍螢锳X、偏移量為BX的地址轉(zhuǎn)換為20bit的地址,并保存于EAX中。

      [上面習(xí)題的答案]

      我們現(xiàn)在可以寫一個真正的程序了。

      經(jīng)典程序:Hello, world

      ;;;應(yīng)該得到一個29字節(jié)的.com文件

      .MODEL TINY.CODE

      CR equ 13 LF equ 10 TERMINATOR equ '$'

      ORG 100h

      Main PROC mov dx,offset sMessage mov ah,9 int 21h mov ax,4c00h int 21h Main ENDP

      sMessage: DB 'Hello, World!' DB CR,LF,TERMINATOR

      END Main;.COM文件的內(nèi)存模型是‘TINY’;代碼段開始

      ;回車;換行

      ;DOS字符串結(jié)束符

      ;代碼起始地址為CS:0100h

      ;令DS:DX指向Message;int 21h(DOS中斷)功能9;終止程序并返回AL的錯誤代碼

      ;程序結(jié)束的同時指定入口點為Main

      那么,我們需要解釋很多東西。

      首先,作為匯編語言的抽象,C語言擁有“指針”這個數(shù)據(jù)類型。在匯編語言中,幾乎所有對內(nèi)存的操作都是由對給定地址的內(nèi)存進行訪問來完成的。這樣,在匯編語言中,絕大多數(shù)操作都要和指針產(chǎn)生或多或少的聯(lián)系。

      這里我想強調(diào)的是,由于這一特性,匯編語言中同樣會出現(xiàn)C程序中常見的緩沖區(qū)溢出問題。如果你正在設(shè)計一個與安全有關(guān)的系統(tǒng),那么最好是仔細檢查你用到的每一個串,例如,它們是否一定能夠以你預(yù)期的方式結(jié)束,以及(如果使用的話)你的緩沖區(qū)是否能保證實際可能輸入的數(shù)據(jù)不被寫入到它以外的地方。作為一個匯編語言程序員,你有義務(wù)檢查每一行代碼的可用性。

      程序中的equ偽指令是宏匯編特有的,它的意思接近于C或Pascal中的const(常量)。多數(shù)情況下,equ偽指令并不為符號分配空間。

      此外,匯編程序執(zhí)行一項操作是非常繁瑣的,通常,在對與效率要求不高的地方,我們習(xí)慣使用系統(tǒng)提供的中斷服務(wù)來完成任務(wù)。例如本例中的中斷21h,它是DOS時代的中斷服務(wù),在Windows中,它也被認為是Windows API的一部分(這一點可以在Microsoft的文檔中查到)。中斷可以被理解為高級語言中的子程序,但又不完全一樣——中斷使用系統(tǒng)棧來保存當前的機器狀態(tài),可以由硬件發(fā)起,通過修改機器狀態(tài)字來反饋信息,等等。

      那么,最后一段通過DB存放的數(shù)據(jù)到底保存在哪里了呢?答案是緊挨著代碼存放。在匯編語言中,DB和普通的指令的地位是相同的。如果你的匯編程序并不知道新的助記符(例如,新的處理器上的CPUID指令),而你很清楚,那么可以用DB 機器碼的方式強行寫下指令。這意味著,你可以超越匯編器的能力撰寫匯編程序,然而,直接用機器碼編程是幾乎肯定是一件費力不討好的事——匯編器廠商會經(jīng)常更新它所支持的指令集以適應(yīng)市場需要,而且,你可以期待你的匯編其能夠產(chǎn)生正確的代碼,因為機器查表是不會出錯的。既然機器能夠幫我們做將程序轉(zhuǎn)換為代碼這件事情,那么為什么不讓它來做呢?

      細心的讀者不難發(fā)現(xiàn),在程序中我們沒有對DS進行賦值。那么,這是否意味著程序的結(jié)果將是不可預(yù)測的呢?答案是否定的。DOS(或Windows中的MS-DOS VM)在加載.com文件的時候,會對寄存器進行很多初始化。.com文件被限制為小于64KB,這樣,它的代碼段、數(shù)據(jù)段都被裝入同樣的數(shù)值(即,初始狀態(tài)下DS=CS)。

      也許會有人說,“嘿,這聽起來不太好,一個64KB的程序能做得了什么呢?還有,你吹得天花亂墜的堆棧段在什么地方?”那么,我們來看看下面這個新的Hello world程序,它是一個EXE文件,在DOS實模式下運行。

      ;;;應(yīng)該得到一個561 字節(jié)的EXE文件

      .MODEL SMALL.STACK 200h

      CR equ 13 LF equ 10 TERMINATOR equ '$'.DATA

      Message DB 'Hello, World!' DB CR,LF,TERMINATOR.CODE

      Main PROC mov ax, DGROUP mov ds, ax

      mov dx, offset Message mov ah, 9 int 21h

      mov ax, 4c00h int 21h Main ENDP END main

      ;采用“SMALL”內(nèi)存模型;堆棧段

      ;回車

      ;換行

      ;DOS字符串結(jié)束符

      ;定義數(shù)據(jù)段

      ;定義顯示串

      ;定義代碼段

      ;將數(shù)據(jù)段;加載到DS寄存器

      ;設(shè)置DX;顯示

      ;終止程序

      561字節(jié)?實現(xiàn)相同功能的程序大了這么多!為什么呢?我們看到,程序擁有了完整的堆棧段、數(shù)據(jù)段、代碼段,其中堆棧段足足占掉了512字節(jié),其余的基本上沒什么變化。

      分成多個段有什么好處呢?首先,它讓程序顯得更加清晰——你肯定更愿意看一個結(jié)構(gòu)清楚的程序,代碼中hard-coded的字符串、數(shù)據(jù)讓人覺得費解。比如,mov dx, 0152h肯定不如mov dx, offset Message來的親切。此外,通過分段你可以使用更多的內(nèi)存,比如,代碼段騰出的空間可以做更多的事情。exe文件另一個吸引人的地方是它能夠?qū)崿F(xiàn)“重定位”。現(xiàn)在你不需要指定程序入口點的地址了,因為系統(tǒng)會找到你的程序入口點,而不是死板的100h。

      程序中的符號也會在系統(tǒng)加載的時候重新賦予新的地址。exe程序能夠保證你的設(shè)計容易地被實現(xiàn),不需要考慮太多的細節(jié)。

      當然,我們的主要目的是將匯編語言作為高級語言的一個有用的補充。如我在開始提到的那樣,真正完全用匯編語言實現(xiàn)的程序不一定就好,因為它不便于維護,而且,由于結(jié)構(gòu)的原因,你也不太容易確保它是正確的;匯編語言是一種非結(jié)構(gòu)化的語言,調(diào)試一個精心設(shè)計的匯編語言程序,即使對于一個老手來說也不啻是一場惡夢,因為你很可能掉到別人預(yù)設(shè)的“陷阱”中——這些技巧確實提高了代碼性能,然而你很可能不理解它,于是你把它改掉,接著就發(fā)現(xiàn)程序徹底敗掉了。使用匯編語言加強高級語言程序時,你要做的通常只是使用匯編指令,而不必搭建完整的匯編程序。絕大多數(shù)(也是目前我遇到的全部)C/C++編譯器都支持內(nèi)嵌匯編,即在程序中使用匯編語言,而不必撰寫單獨的匯編語言程序——這可以節(jié)省你的不少精力,因為前面講述的那些偽指令,如equ等,都可以用你熟悉的高級語言方式來編寫,編譯器會把它轉(zhuǎn)換為適當?shù)男问健?/p>

      需要說明的是,在高級語言中一定要注意編譯結(jié)果。編譯器會對你的匯編程序做一些修改,這不一定符合你的要求(附帶說一句,有時編譯器會很聰明地調(diào)整指令順序來提高性能,這種情況下最好測試一下哪種寫法的效果更好),此時需要做一些更深入的修改,或者用db來強制編碼。

      3.2 保護模式

      實模式的東西說得太多了,盡管我已經(jīng)刪掉了許多東西,并把一些原則性的問題拿到了這一節(jié)討論。這樣做不是沒有理由的——保護模式才是現(xiàn)在的程序(除了操作系統(tǒng)的底層啟動代碼)最常用的CPU模式。保護模式提供了很多令人耳目一新的功能,包括內(nèi)存保護(這是保護模式這個名字的來源)、進程支持、更大的內(nèi)存支持,等等。

      對于一個編程人員來說,能“偷懶”是一件令人愉快的事情。這里“偷懶”是說把“應(yīng)該”由系統(tǒng)做的事情做的事情全都交給系統(tǒng)。為什么呢?這出自一個基本思想——人總有犯錯誤的時候,然而規(guī)則不會,正確地了解規(guī)則之后,你可以期待它像你所了解的那樣執(zhí)行。對于C程序來說,你自己用C語言寫的實現(xiàn)相同功能的函數(shù)通常沒有系統(tǒng)提供的函數(shù)性能好(除非你用了比函數(shù)庫好很多的算法),因為系統(tǒng)的函數(shù)往往使用了更好的優(yōu)化,甚至可能不是用C語言直接編寫的。

      當然,“偷懶”的意思是說,把那些應(yīng)該讓機器做的事情交給計算機來做,因為它做得更好。我們應(yīng)該把精力集中到設(shè)計算法,而不是編寫源代碼本身上,因為編譯器幾乎只能做等價優(yōu)化,而實現(xiàn)相同功能,但使用更好算法的程序?qū)崿F(xiàn),則幾乎只能由人自己完成。

      舉個例子,這樣一個函數(shù):

      int fun(){ int a=0;register int i;for(i=0;i<1000;i++)a+=i;return a;}

      在某種編譯模式[DEBUG]下被編譯為

      push ebp mov ebp,esp sub esp,48h push ebx push esi push edi lea edi,[ebp-48h] mov ecx,12h mov eax,0CCCCCCCCh rep stos dword ptr [edi] mov dword ptr [ebp-4],0 mov dword ptr [ebp-8],0 jmp fun+31h mov eax,dword ptr [ebp-8] add eax,1 mov dword ptr [ebp-8],eax cmp dword ptr [ebp-8],3E8h jge fun+45h mov ecx,dword ptr [ebp-4] add ecx,dword ptr [ebp-8] mov dword ptr [ebp-4],ecx jmp fun+28h mov eax,dword ptr [ebp-4] pop edi pop esi pop ebx mov esp,ebp pop ebp ret;子程序入口

      ;保護現(xiàn)場

      ;初始化變量-調(diào)試版本特有。

      ;本質(zhì)是在堆中挖一塊地兒,存CCCCCCCC。;用串操作進行,這將發(fā)揮Intel處理器優(yōu)勢;‘a(chǎn)=0’;‘i=0’

      ;走著;i++

      ;i<1000?

      ;a+=i;

      ;return a;

      ;恢復(fù)現(xiàn)場

      ;返回

      而在另一種模式[RELEASE/MINSIZE]下卻被編譯為

      xor eax,eax xor ecx,ecx add eax,ecx inc ecx cmp ecx,3E8h jl fun+4 ret;a=0;;i=0;;a+=i;;i++;;i<1000?;是->繼續(xù)繼續(xù);return a

      如果讓我來寫,多半會寫成

      mov eax, 079f2ch ret;return 499500

      為什么這樣寫呢?我們看到,i是一個外界不能影響、也無法獲知的內(nèi)部狀態(tài)量。作為這段程序來說,對它的計算對于結(jié)果并沒有直接的影響——它的存在不過是方便算法描述而已。并且我們看到的,這段程序?qū)嶋H上無論執(zhí)行多少次,其結(jié)果都不會發(fā)生變化,因此,直接返回計算結(jié)果就可以了,計算是多余的(如果說一定要算,那么應(yīng)該是編譯器在編譯過程中完成它)。

      更進一步,我們甚至希望編譯器能夠直接把這個函數(shù)變成一個符號常量,這樣連操作堆棧的過程也省掉了。

      第三種結(jié)果屬于“等效”代碼,而不是“等價”代碼。作為用戶,很多時候是希望編譯器這樣做的,然而由于目前的技術(shù)尚不成熟,有時這種做法會造成一些問題(gcc和g++的頂級優(yōu)化可以造成編譯出的FreeBSD內(nèi)核行為異常,這是我在FreeBSD上遇到的唯一一次軟件原因的kernel panic),因此,并不是所有的編譯器都這樣做(另一方面的原因是,如果編譯器在這方面做的太過火,例如自動求解全部“固定”問題,那么如果你的程序是解決固定的問題“很大”,如求解迷宮,那么在編譯過程中你就會找錘子來砸計算機了)。然而,作為編譯器制造商,為了提高自己的產(chǎn)品的競爭力,往往會使用第三種代碼來做函數(shù)庫。正如前面所提到的那樣,這種優(yōu)化往往不是編譯器本身的作用,盡管現(xiàn)代編譯程序擁有編譯執(zhí)行、循環(huán)代碼外提、無用代碼去除等諸多優(yōu)化功能,但它都不能保證程序最優(yōu)。最后一種代碼恐怕很少有編譯器能夠做到,不信你可以用自己常用的編譯器加上各種優(yōu)化選項試試:)

      發(fā)現(xiàn)什么了嗎?三種代碼中,對于內(nèi)存的訪問一個比一個少。這樣做的理由是,盡可能地利用寄存器并減少對內(nèi)存的訪問,可以提高代碼性能。在某些情況下,使代碼既小又快是可能的。

      書歸正傳,我們來說說保護模式的內(nèi)存模型。保護模式的內(nèi)存和實模式有很多共同之處。

      毫無疑問,以'protected mode'(保護模式), 'global descriptor table'(全局描述符表), 'local descriptor table'(本地描述符表)和'selector'(選擇器)搜索,你會得到完整介紹它們的大量信息。

      保護模式與實模式的內(nèi)存類似,然而,它們之間最大的區(qū)別就是保護模式的內(nèi)存是“線性”的。

      新的計算機上,32-bit的寄存器已經(jīng)不是什么新鮮事(如果你哪天聽說你的CPU的寄存器不是32-bit的,那么它——簡直可以肯定地說——的字長要比32-bit還要多。新的個人機上已經(jīng)開始逐步采用64-bit的CPU了),換言之,實際上段/偏移量這一格局已經(jīng)不再需要了。盡管如此,在繼續(xù)看保護模式內(nèi)存結(jié)構(gòu)時,仍請記住段/偏移量的概念。不妨把段寄存器看作對于保護模式中的選擇器的一個模擬。選擇器是全局描述符表(Global Descriptor Table, GDT)或本地描述符表(Local Descriptor Table, LDT)的一個指針。

      如圖所示,GDT和LDT的每一個項目都描述一塊內(nèi)存。例如,一個項目中包含了某塊被描述的內(nèi)存的物理的基地址、長度,以及其他一些相關(guān)信息。

      保護模式是一個非常重要的概念,同時也是目前撰寫應(yīng)用程序時,最常用的CPU模式(運行在新的計算機上的操作系統(tǒng)很少有在實模式下運行的)。

      為什么叫保護模式呢?它“保護”了什么?答案是進程的內(nèi)存。保護模式的主要目的在于允許多個進程同時運行,并保護它們的內(nèi)存不受其他進程的侵犯。這有點類似于C++中的機制,然而它的強制力要大得多。如果你的進程在保護模式下以不恰當?shù)姆绞皆L問了內(nèi)存(例如,寫了“只讀”內(nèi)存,或讀了不可讀的內(nèi)存,等等),那么CPU就會產(chǎn)生一個異常。這個異常將交給操作系統(tǒng)處理,而這種處理,假如你的程序沒有特別說明操作系統(tǒng)該如何處理的話,一般就是殺掉做錯了事情的進程。

      我像這樣的對話框大家一定非常熟悉(臨時寫了一個程序故意造成的錯誤):

      好的,只是一個程序崩潰了,而操作系統(tǒng)的其他進程照常運行(同樣的程序在DOS中幾乎是板上釘釘?shù)乃罊C,因為NULL指針的位置恰好是中斷向量表),你甚至還可以調(diào)試它。

      保護模式還有其他很多好處,在此就不一一贅述了。實模式和保護模式之間的切換問題我打算放在后面的“高級技巧”一章來講,因為多數(shù)程序并不涉及這個。

      了解了內(nèi)存的格局,我們就可以進入下一節(jié)——操作內(nèi)存了。

      3.3 操作內(nèi)存

      前兩節(jié)中,我們介紹了實模式和保護模式中使用的不同的內(nèi)存格局?,F(xiàn)在開始解釋如何使用這些知識。

      回憶一下前面我們說過的,寄存器可以用作內(nèi)存指針?,F(xiàn)在,是他們發(fā)揮作用的時候了。

      可以將內(nèi)存想象為一個順序的字節(jié)流。使用指針,可以任意地操作(讀寫)內(nèi)存。

      現(xiàn)在我們需要一些其他的指令格式來描述對于內(nèi)存的操作。操作內(nèi)存時,首先需要的就是它的地址。

      讓我們來看看下面的代碼:

      mov ax,[0]

      方括號表示,里面的表達式指定的不是立即數(shù),而是偏移量。在實模式中,DS:0中的那個字(16-bit長)將被裝入AX。

      然而0是一個常數(shù),如果需要在運行的時候加以改變,就需要一些特殊的技巧,比如程序自修改。匯編支持這個特性,然而我個人并不推薦這種方法——自修改大大降低程序的可讀性,并且還降低穩(wěn)定性,性能還不一定好。我們需要另外的技術(shù)。

      mov bx,0 mov ax,[bx]

      看起來舒服了一些,不是嗎?BX寄存器的內(nèi)容可以隨時更改,而不需要用冗長的代碼去修改自身,更不用擔(dān)心由此帶來的不穩(wěn)定問題。

      同樣的,mov指令也可以把數(shù)據(jù)保存到內(nèi)存中:

      mov [0],ax

      在存儲器與寄存器之間交換數(shù)據(jù)應(yīng)該足夠清楚了。

      有些時候我們會需要操作符來描述內(nèi)存數(shù)據(jù)的寬度: 操作符 意義

      byte ptr 一個字節(jié)(8-bit, 1 byte)word ptr 一個字(16-bit)dword ptr 一個雙字(32-bit)

      例如,在DS:100h處保存1234h,以字存放:

      mov word ptr [100h],01234h

      于是我們將mov指令擴展為:

      mov reg(8,16,32), mem(8,16,32)mov mem(8,16,32), reg(8,16,32)mov mem(8,16,32), imm(8,16,32)

      需要說明的是,加減同樣也可以在[]中使用,例如:

      mov ax,[bx+10] mov ax,[bx+si] mov ax,es:[di+bp]

      等等。我們看到,對于內(nèi)存的操作,即使使用MOV指令,也有許多種可能的方式。下一節(jié)中,我們將介紹如何操作串。

      感謝 網(wǎng)友 水杉 指出此答案中的一處錯誤。感謝 Heallven 指出.COM程序?qū)嵗幾g失敗的問題

      我們前面已經(jīng)提到,內(nèi)存可以和寄存器交換數(shù)據(jù),也可以被賦予立即數(shù)。問題是,如果我們需要把內(nèi)存的某部分內(nèi)容復(fù)制到另一個地址,又怎么做呢?

      設(shè)想將DS:SI處的連續(xù)512字節(jié)內(nèi)容復(fù)制到ES:DI(先不考慮可能的重疊)。也許會有人寫出這樣的代碼:

      NextByte: mov cx,512 mov al,ds:[si] mov es:[di],al inc si inc di loop NextByte;循環(huán)次數(shù)

      我不喜歡上面的代碼。它的確能達到作用,但是,效率不好。如果你是在做優(yōu)化,那么寫出這樣的代碼意味著賠了夫人又折兵。

      Intel的CPU的強項是串操作。所謂串操作就是由CPU去完成某一數(shù)量的、重復(fù)的內(nèi)存操作。需要說明的是,我們常用的KMP算法(用于匹配字符串中的模式)的改進——Boyer算法,由于沒有利用串操作,因此在Intel的CPU上的效率并非最優(yōu)。好的編譯器往往可以利用Intel CPU的這一特性優(yōu)化代碼,然而,并非所有的時候它都能產(chǎn)生最好的代碼。某些指令可以加上REP前綴(repeat, 反復(fù)之意),這些指令通常被叫做串操作指令。舉例來說,STOSD指令將EAX的內(nèi)容保存到ES:DI,同時在DI上加或減四。類似的,STOSB和STOSW分別作1字節(jié)或1字的上述操作,在DI上加或減的數(shù)是1或2。

      計算機語言通常是不允許二義性的。為什么我要說“加或減”呢?沒錯,孤立地看STOS?指令,并不能知道到底是加還是減,因為這取決于“方向”標志(DF, Direction Flag)。如果DF被復(fù)位,則加;反之則減。

      置位、復(fù)位的指令分別是STD和CLD。

      當然,REP只是幾種可用前綴之一。常用的還包括REPNE,這個前綴通常被用來比較兩個串,或搜索某個特定字符(字、雙字)。REPZ、REPE、REPNZ也是非常常用的指令前綴,分別代表ZF(Zero Flag)在不同狀態(tài)時重復(fù)執(zhí)行。下面說三個可以復(fù)制數(shù)據(jù)的指令: 助記符 意義

      movsb 將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI++、DI++ movsw 將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI+=

      2、DI+=2 movsd 將DS:SI的一字節(jié)復(fù)制到ES:DI,之后SI+=

      4、DI+=4 于是上面的程序改寫為 cld mov cx, 128 rep movsd;復(fù)位DF;512/4 = 128,共128個雙字;行動!

      第一句cld很多時候是多余的,因為實際寫程序時,很少會出現(xiàn)置DF的情況。不過在正式?jīng)Q定刪掉它之前,建議你仔細地調(diào)試自己的程序,并確認每一個能夠走到這里的路徑中都不會將DF置位。

      錯誤(非預(yù)期的)的DF是危險的。它很可能斷送掉你的程序,因為這直接造成緩沖區(qū)溢出問題。

      什么是緩沖區(qū)溢出呢?緩沖區(qū)溢出分為兩類,一類是寫入緩沖區(qū)以外的內(nèi)容,一類是讀取緩沖區(qū)以外的內(nèi)容。后一種往往更隱蔽,但隨便哪一個都有可能斷送掉你的程序。

      緩沖區(qū)溢出對于一個網(wǎng)絡(luò)服務(wù)來說很可能更加危險。懷有惡意的用戶能夠利用它執(zhí)行自己希望的指令。服務(wù)通常擁有更高的特權(quán),而這很可能會造成特權(quán)提升;即使不能提升攻擊者擁有的特權(quán),他也可以利用這種問題使服務(wù)崩潰,從而形成一次成功的DoS(拒絕服務(wù))攻擊。每年CERT的安全公告中,都有6成左右的問題是由于緩沖區(qū)溢出造成的。

      在使用匯編語言,或C語言編寫程序時,很容易在無意中引入緩沖區(qū)溢出。然而并不是所有的語言都會引入緩沖區(qū)溢出問題,Java和C#,由于沒有指針,并且緩沖區(qū)采取動態(tài)分配的方式,有效地消除了造成緩沖區(qū)溢出的土壤。

      匯編語言中,由于REP*前綴都用CX作為計數(shù)器,因此情況會好一些(當然,有時也會更糟糕,因為由于CX的限制,很可能使原本可能改變程序行為的緩沖區(qū)溢出的范圍縮小,從而更為隱蔽)。避免緩沖區(qū)溢出的一個主要方法就是仔細檢查,這包括兩方面:設(shè)置合理的緩沖區(qū)大小,和根據(jù)大小編寫程序。除此之外,非常重要的一點就是,在匯編語言這個級別寫程序,你肯定希望去掉所有的無用指令,然而再去掉之前,一定要進行嚴格的測試;更進一步,如果能加上注釋,并通過善用宏來做調(diào)試模式檢查,往往能夠達到更好的效果。

      正如3.2節(jié)提到到的那樣,保護模式中,你可以使用32位的線性地址,這意味著直接訪問4GB的內(nèi)存。由于這個原因,選擇器不用像實模式中段寄存器那樣頻繁地修改。順便提一句,這份教程中所說的保護模式指的是386以上的保護模式,或者,Microsoft通常稱為“增強模式”的那種。

      在為選擇器裝入數(shù)值的時候一定要非常小心。錯誤的數(shù)值往往會導(dǎo)致無效頁面錯誤(在Windows中經(jīng)常出現(xiàn):)。同時,也不要忘記你的地址是32位的,這也是保護模式的主要優(yōu)勢之一。

      現(xiàn)在假設(shè)存在一個描述符描述從物理的0:0開始的全部內(nèi)存,并已經(jīng)加載進DS(數(shù)據(jù)選擇器),則我們可以通過下面的程序來操作VGA的VRAM: mov edi,0a0000h mov byte ptr [edi],0fh;VGA顯存的偏移量;將第一字節(jié)改為0fh 很明顯,這比實模式下的程序 mov ax,0a000h mov ds,ax mov di,0 mov [di],0fh;AX-> VGA段地址;將AX值載入DS;DI清零

      ;修改第一字節(jié) 看上去要舒服一些。

      到目前為止,您已經(jīng)了解了基本的寄存器以及內(nèi)存的操作知識。事實上,您現(xiàn)在已經(jīng)可以寫出很多的底層數(shù)據(jù)處理程序了。

      下面我來說說堆棧。堆棧實在不是一個讓人陌生的數(shù)據(jù)結(jié)構(gòu),它是一個先進后出(FILO)的線性表,能夠幫助你完成很多很好的工作。

      先進后出(FILO)是這樣一個概念:最后放進表中 的數(shù)據(jù)在取出時最先出來。先進后出(FILO)和先 進先出(FIFO, 和先進后出的規(guī)則相反),以及隨 機存取是最主要的三種存儲器訪問方式。

      對于堆棧而言,最后放入的數(shù)據(jù)在取出時最先出

      現(xiàn)。對于子程序調(diào)用,特別是遞歸調(diào)用來說,這 是一個非常有用的特性。

      一個鐵桿的匯編語言程序員有時會發(fā)現(xiàn)系統(tǒng)提供的寄存器不夠。很顯然,你可以使用普通的內(nèi)存操作來完成這個工作,就像C/C++中所做的那樣。

      沒錯,沒錯,可是,如果數(shù)據(jù)段(數(shù)據(jù)選擇器)以及偏移量發(fā)生變化怎么辦?更進一步,如果希望保存某些在這種操作中可能受到影響的寄存器的時候怎么辦?確實,你可以把他們也存到自己的那片內(nèi)存中,自己實現(xiàn)堆棧。

      太麻煩了??

      既然系統(tǒng)提供了堆棧,并且性能比自己寫一份更好,那么為什么不直接加以利用呢 系統(tǒng)堆棧不僅僅是一段內(nèi)存。由于CPU對它實施管理,因此你不需要考慮堆棧指針的修正問題??梢园鸭拇嫫鲀?nèi)容,甚至一個立即數(shù)直接放到堆棧里,并在需要的時候?qū)⑵淙〕?。同時,系統(tǒng)并不要求取出的數(shù)據(jù)仍然回到原來的位置。

      除了顯式地操作堆棧(使用PUSH和POP指令)之外,很多指令也需要使用堆棧,如INT、CALL、LEAVE、RET、RETF、IRET等等。配對使用上述指令并不會造成什么問題,然而,如果你打算使用LEAVE、RET、RETF、IRET這樣的指令實現(xiàn)跳轉(zhuǎn)(比JMP更為麻煩,然而有時,例如在加密軟件中,或者需要修改調(diào)用者狀態(tài)時,這是必要的)的話,那么我的建議是,先搞清楚它們做的到底是什么,并且,精確地了解自己要做什么。正如前面所說的,有兩個顯式地操作堆棧的指令:

      助記符 功能

      PUSH 將操作數(shù)存入堆棧,同時修正堆棧指針

      POP 將棧頂內(nèi)容取出并存到目的操作數(shù)中,同時修正堆棧指針 我們現(xiàn)在來看看堆棧的操作。執(zhí)行之前 執(zhí)行代碼 mov ax,1234h mov bx,10 push ax push bx 之后,堆棧的狀態(tài)為 之后,再執(zhí)行 pop dx pop cx 堆棧的狀態(tài)成為

      當然,dx、cx中的內(nèi)容將分別是000ah和1234h。

      注意,最后這張圖中,我沒有抹去1234h和000ah,因為POP指令并不從內(nèi)存中抹去數(shù)值。不過盡管如此,我個人仍然非常反對繼續(xù)使用這兩個數(shù)(你可以通過修改SP來再次POP它們),然而這很容易導(dǎo)致錯誤。

      一定要保證堆棧段有足夠的空間來執(zhí)行中斷,以及其他一些隱式的堆棧操作。僅僅統(tǒng)計PUSH的數(shù)量并據(jù)此計算堆棧所需的大小很可能造成問題。CALL指令將返回地址放到堆棧中。絕大多數(shù)C/C++編譯器提供了“堆棧檢查”這個編譯選項,其作用在于保證C程序段中沒有忘記對堆棧中多余的數(shù)據(jù)進行清理,從而保證返回地址有效。本章小結(jié)

      本章中介紹了內(nèi)存的操作的一些入門知識。限于篇幅,我不打算展開細講指令,如cmps*,lods*,stos*,等等。這些指令的用法和前面介紹的movs*基本一樣,只是有不同的作用而已。

      4.0 利用子程序與中斷

      已經(jīng)掌握了匯編語言?沒錯,你現(xiàn)在已經(jīng)可以去破譯別人代碼中的秘密。然而,我們還有一件重要的東西沒有提到,那就是自程序和中斷。這兩件東西是如此的重要,以至于你的程序幾乎不可能離開它們。

      4.1 子程序

      在高級語言中我們經(jīng)常要用到子程序。高級語言中,子程序是如此的神奇,我們能夠定義和主程序,或其他子程序一樣的變量名,而訪問不同的變量,并且,還不和程序的其他部分相沖突。

      然而遺憾的是,這種“優(yōu)勢”在匯編語言中是不存在的。

      匯編語言并不注重如何減輕程序員的負擔(dān);相反,匯編語言依賴程序員的良好設(shè)計,以期發(fā)揮CPU的最佳性能。匯編語言不是結(jié)構(gòu)化的語言,因此,它不提供直接的“局部變量”。如果需要“局部變量”,只能通過堆或棧自行實現(xiàn)。

      從這個意義上講,匯編語言的子程序更像GWBASIC中的GOSUB調(diào)用的那些“子程序”。所有的“變量”(本質(zhì)上,屬于進程的內(nèi)存和寄存器)為整個程序所共享,高級語言編譯器所做的,將局部變量放到堆或棧中的操作,只能自行實現(xiàn)。

      參數(shù)的傳遞是靠寄存器和堆棧來完成的。高級語言中,子程序(函數(shù)、過程,或類似概念的東西)依賴于堆和棧來傳遞。

      讓我們來簡單地分析一下一般高級語言的子程序的執(zhí)行過程。無論C、C++、BASIC、Pascal,這一部分基本都是一致的。

      調(diào)用者將子程序執(zhí)行完成時應(yīng)返回的地址、參數(shù)壓入堆棧

      子程序使用BP指針+偏移量對棧中的參數(shù)尋址,并取出、完成操作

      子程序使用RET或RETF指令返回。此時,CPU將IP置為堆棧中保存的地址,并繼續(xù)予以執(zhí)行

      毋庸置疑,堆棧在整個過程中發(fā)揮著非常重要的作用。不過,本質(zhì)上對子程序最重要的還是返回地址。如果子程序不知道這個地址,那么系統(tǒng)將會崩潰。

      調(diào)用子程序的指令是CALL,對應(yīng)的返回指令是RET。此外,還有一組指令,即ENTER和LEAVE,它們可以幫助進行堆棧的維護。

      CALL指令的參數(shù)是被調(diào)用子程序的地址。使用宏匯編的時候,這通常是一個標號。CALL和RET,以及ENTER和LEAVE配對,可以實現(xiàn)對于堆棧的自動操作,而不需要程序員進行PUSH/POP,以及跳轉(zhuǎn)的操作,從而提高了效率。

      作為一個編譯器的實現(xiàn)實例,我用Visual C++編譯了一段C++程序代碼,這段匯編代碼是使用特定的編譯選項得到的結(jié)果,正常的RELEASE代碼會比它精簡得多。包含源代碼的部分反匯編結(jié)果如下(取自Visual C++調(diào)試器的運行結(jié)果,我刪除了10條int 3指令,并加上了一些注釋,除此之外,沒有做任何修改): 1: int myTransform(int nInput){ 00401000 push ebp;保護現(xiàn)場原先的EBP指針 00401001 mov ebp,esp 2: return(nInput*2 + 3)% 7;00401003 mov eax,dword ptr [nInput];取參數(shù)

      00401006 lea eax,[eax+eax+3];LEA比ADD加法更快

      0040100A cdq;DWORD->QWORD(擴展字長)0040100B mov ecx,7;除數(shù) 00401010 idiv eax,ecx;除

      00401012 mov eax,edx;商->eax(eax中保存返回值)3: } 00401014 pop ebp;恢復(fù)現(xiàn)場的ebp指針

      00401015 ret;返回

      ;此處刪除10條int 3指令,它們是方便調(diào)試用的,并不影響程序行為。4: 5: int main(int argc, char* argv[])6: { 00401020 push ebp;保護現(xiàn)場原先的EBP指針 00401021 mov ebp,esp 00401023 sub esp,10h;為取argc, argv修正堆棧指針。7: int a[3];8: for(register int i=0;i<3;i++){ 00401026 mov dword ptr [i],0;0->i 0040102D jmp main+18h(00401038);判斷循環(huán)條件 0040102F mov eax,dword ptr [i];i->eax 00401032 add eax,1;eax ++ 00401035 mov dword ptr [i],eax;eax->i 00401038 cmp dword ptr [i],3;循環(huán)條件: i與3比較

      0040103C jge main+33h(00401053);如果不符合條件,則應(yīng)結(jié)束循環(huán) 9: a[i] = myTransform(i);0040103E mov ecx,dword ptr [i];i->ecx 00401041 push ecx;ecx(i)-> 堆棧 00401042 call myTransform(00401000);調(diào)用myTransform 00401047 add esp,4;esp+=4: 在堆中的新單元;準備存放返回結(jié)果 0040104A mov edx,dword ptr [i];i->edx 0040104D mov dword ptr a[edx*4],eax;將eax(myTransform返回值);放回a[i] 10: } 00401051 jmp main+0Fh(0040102f);計算i++,并繼續(xù)循環(huán) 11: return 0;00401053 xor eax,eax;返回值應(yīng)該是0 12: } 00401055 mov esp,ebp;恢復(fù)堆棧指針 00401057 pop ebp;恢復(fù)BP 00401058 ret;返回調(diào)用者(C++運行環(huán)境)上述代碼確實做了一些無用功,當然,這是因為編譯器沒有對這段代碼進行優(yōu)化。讓我們來關(guān)注一下這段代碼中,是如何調(diào)用子程序的。不考慮myTransform這個函數(shù)實際進行的數(shù)值運算,最讓我感興趣的是這一行代碼:

      00401003 mov eax,dword ptr [nInput];取參數(shù) 這里nInput是一個簡簡單單的變量符號嗎?Visual C++的調(diào)試器顯然不能告訴我們答案——它的設(shè)計目標是為了方便程序調(diào)試,而不是向你揭示編譯器生成的代碼的實際構(gòu)造。我用另外一個反匯編器得到的結(jié)果是:

      00401003 mov eax,dword ptr [ebp+8];取參數(shù)

      這和我們在main()中看到的壓棧順序是完全吻合的(注意,程序運行到這個地方的時候,EBP=ESP)。main()最終將i的值通過堆棧傳遞給了myTransform()。

      剖析上面的程序只是說明了我前面所提到的子程序的一部分用法。對于匯編語言來說,完全沒有必要拘泥于結(jié)構(gòu)化程序設(shè)計的框架(在今天,使用匯編的主要目的在于提高執(zhí)行效率,而不是方便程序的維護和調(diào)試,因為匯編不可能在這一點上做得比C++更好)。考慮下面的程序:

      void myTransform1(int nCount, char* sBytes){ for(register int i=1;i

      void myTransform2(int nCount, char* sBytes){ for(register int i=0;i

      很容易看出,這兩個函數(shù)包含了公共部分,即

      for(i=0;i

      目前,還沒有編譯器能夠做到將這兩部分合并。依然沿用剛才的編譯選項,得到的反匯編結(jié)果是(同樣地刪除了int 3):

      1: void myTransform1(int nCount, char* sBytes){ 00401000 push ebp 00401001 mov ebp,esp 00401003 push ecx 2: for(register int i=1;i

      非常明顯地,0040103d-0040106e和00401074-004010a5這兩段代碼存在少量的差別,但很顯然只是對寄存器的偏好不同(編譯器在優(yōu)化時,這可能會減少堆棧操作,從而提高性能,但在這里只是使用了不同的寄存器而已)

      對代碼進行合并的好處是非常明顯的。新的操作系統(tǒng)往往使用頁式內(nèi)存管理。當內(nèi)存不足時,程序往往會頻繁引發(fā)頁面失效(Page faults),從而引發(fā)操作系統(tǒng)從磁盤中讀取一些東西。磁盤的速度趕不上內(nèi)存的速度,因此,這一行為將導(dǎo)致性能的下降。通過合并一部分代碼,可以減少程序的大小,這意味著減少頁面失效的可能性,從而軟件的性能會有所提高 當然,這樣做的代價也不算低——你的程序?qū)⒆兊秒y懂,并且難于維護。因此,再進行這樣的優(yōu)化之前,一定要注意:

      優(yōu)化前的程序必須是正確的。如果你不能確保這一點,那么這種優(yōu)化必將給你的調(diào)試帶來極大的麻煩。

      優(yōu)化前的程序?qū)崿F(xiàn)最好是最優(yōu)的。仔細檢查你的設(shè)計,看看是否已經(jīng)使用了最合適(即,對于此程序而言最優(yōu))的算法,并且已經(jīng)在高級語言許可的范圍內(nèi)進行了最好的實現(xiàn)。優(yōu)化最好能夠非常有效地減少程序大小(例如,如果只是減少十幾個字節(jié),恐怕就沒什么必要了),或非常有效地提高程序的運行速度(如果代碼只是運行一次,并且只是節(jié)省幾個時鐘周期,那么在多數(shù)場合都沒有意義)。否則,這種優(yōu)化將得不償失。4.2 中斷

      中斷應(yīng)該說是一個陳舊的話題。在新的系統(tǒng)中,它的作用正在逐漸被削弱,而變成操作系統(tǒng)專用的東西。并不是所有的計算機系統(tǒng)都提供中斷,然而在x86系統(tǒng)中,它的作用是不可替代的。

      中斷實際上是一類特殊的子程序。它通常由系統(tǒng)調(diào)用,以響應(yīng)突發(fā)事件。

      例如,進行磁盤操作時,為了提高性能,可能會使用DMA方式進行操作。CPU向DMA控制器發(fā)出指令,要求外設(shè)和內(nèi)存直接交換數(shù)據(jù),而不通過CPU。然后,CPU轉(zhuǎn)去進行起他的操作;當數(shù)據(jù)交換結(jié)束時,CPU可能需要進行一些后續(xù)操作,但此時它如何才能知道DMA已經(jīng)完成了操作呢?

      很顯然不是依靠CPU去查詢狀態(tài)——這樣DMA的優(yōu)勢就不明顯了。為了盡可能地利用DMA的優(yōu)勢,在完成DMA操作的時候,DMA會告訴CPU“這事兒我辦完了”,然后CPU會根據(jù)需要進行處理。

      這種處理可能很復(fù)雜,需要若干條指令來完成。子程序是一個不錯的主意,不過,CALL指令需要指定地址,讓外設(shè)強迫CPU執(zhí)行一條CALL指令也違背了CPU作為核心控制單元的設(shè)計初衷。考慮到這些,在x86系統(tǒng)中引入了中斷向量的概念。

      中斷向量表是保存在系統(tǒng)數(shù)據(jù)區(qū)(實模式下,是0:0開始的一段區(qū)域)的一組指針。這組指針指向每一個中斷服務(wù)程序的地址。整個中斷向量表的結(jié)構(gòu)是一個線性表。

      每一個中斷服務(wù)有自己的唯一的編號,我們通常稱之為中斷號。每一個中斷號對應(yīng)中斷向量表中的一項,也就是一個中斷向量。外設(shè)向CPU發(fā)出中斷請求,而CPU自己將根據(jù)當前的程序狀態(tài)決定是否中斷當前程序并調(diào)用相應(yīng)的中斷服務(wù)。不難根據(jù)造成中斷的原因?qū)⒅袛喾譃閮深悾河布袛嗪蛙浖袛唷S布袛嘤泻芏喾诸惙椒?,如根?jù)是否可以屏蔽分類、根據(jù)優(yōu)先級高低分類,等等??紤]到這些分類并不一定科學(xué),并且對于我們介紹中斷的使用沒有太大的幫助,因此我并不打算太詳細地介紹它(在本教程的高級篇中,關(guān)于加密解密的部分會提到某些硬件中斷的利用,但那是后話)。

      在設(shè)計操作系統(tǒng)時,中斷向量的概念曾經(jīng)帶來過很大的便利。操作系統(tǒng)隨時可能升級,這樣,通過CALL來調(diào)用操作系統(tǒng)的服務(wù)(如果說每個程序都包含對于文件系統(tǒng)、進程表這些應(yīng)該由操作系統(tǒng)管理的數(shù)據(jù)的直接操作的話,不僅會造成程序的臃腫,而且不利于系統(tǒng)的安全)就顯得不太合適了——沒人能知道,以后的操作系統(tǒng)的服務(wù)程序入口點會不會是那兒。軟件中斷的存在為解決這個問題提供了方便。對于一臺包含了BIOS的計算機來說,啟動的時候系統(tǒng)已經(jīng)提供了一部分服務(wù),例如顯示服務(wù)。無論你的BIOS、顯示卡有多么的“個性”,只要他們和IBM PC兼容,那么此時你肯定可以通過調(diào)用16(10h)號中斷來使用顯示服務(wù)。調(diào)用中斷的指令是

      int 中斷號

      這將引發(fā)CPU去調(diào)用一個中斷。CPU將保存當前的程序狀態(tài)字,清除Trap和Interrupt兩個標志,將即將執(zhí)行的指令地址壓入堆棧,并調(diào)用中斷服務(wù)(根據(jù)中斷向量表)。

      編寫中斷服務(wù)程序不是一件容易的事情。很多時候,中斷服務(wù)程序必須寫成可重入代碼(或純代碼,pure code)。所謂可重入代碼是指,程序的運行過程中可以被打斷,并由開始處再次執(zhí)行,并且在合理的范圍內(nèi)(多次重入,而不造成堆棧溢出等其他問題),程序可以在被打斷處繼續(xù)執(zhí)行,并且執(zhí)行結(jié)果不受影響。

      由于在多線程環(huán)境中等其他一些地方進行程序設(shè)計時也需要考慮這個因素,因此這里著重講一下可重入代碼的編寫。

      可重入代碼最主要的要求就是,程序不應(yīng)使用某個指定的內(nèi)存地址的內(nèi)存(對于高級語言來說,這通常是全局變量,或?qū)ο蟮某蓡T)。如果可能的話,應(yīng)使用寄存器,或其他方式來解決。如果不能做到這一點,則必須在開始、結(jié)束的時候分別禁止和啟用中斷,并且,運行時間不能太長。

      下面用C語言分別舉一個可重入函數(shù),和兩個非可重入函數(shù)的例子(注.這些例子應(yīng)該是在某本多線程或操作系統(tǒng)的書上看到的,遺憾的是我想不起來是哪本書了,在這里先感謝那位作者提供的范例):

      可重入函數(shù):

      void strcpy(char* lpszDest, char* lpszSrc){ while(*dest++=*src++);*dest=0;}

      非可重入函數(shù)

      char cTemp;// 全局變量

      void SwapChar(char* lpcX, char* lpcY){ cTemp = *lpcX;*lpcX = *lpcY;lpcY = cTemp;// 引用了全局變量,在分享內(nèi)存的多個線程中可能造成問題 }

      非可重入函數(shù)

      void SwapChar2(char* lpcX, char* lpcY){ static char cTemp;// 靜態(tài)變量

      cTemp = *lpcX;*lpcX = *lpcY;lpcY = cTemp;// 引用了靜態(tài)變量,在分享內(nèi)存的多個線程中可能造成問題 }

      中斷利用的是系統(tǒng)的棧。棧操作是可重入的(因為??梢员WC“先進后出”),因此,我們并不需要考慮棧操作的重入問題。使用宏匯編器寫出可重入的匯編代碼需要注意一些問題。簡單地說,干脆不要用標號作為變量是一個不錯的主意。

      使用高級語言編寫可重入程序相對來講輕松一些。把持住不訪問那些全局(或當前對象的)變量,不使用靜態(tài)局部變量,堅持只適用局部變量,寫出的程序就將是可重入的。書歸正傳,調(diào)用軟件中斷時,通常都是通過寄存器傳進、傳出參數(shù)。這意味著你的int指令周圍也許會存在一些“幫手”,比如下面的代碼: mov ax, 4c00h int 21h 就是通過調(diào)用DOS中斷服務(wù)返回父進程,并帶回錯誤反饋碼0。其中,ax中的數(shù)據(jù)4c00h就是傳遞給DOS中斷服務(wù)的參數(shù)。

      到這里,x86匯編語言的基礎(chǔ)部分就基本上講完了,《簡明x86匯編語言教程》的初級篇——匯編語言基礎(chǔ)也就到此告一段落。當然,目前為止,我只是蜻蜓點水一般提到了一些學(xué)習(xí)x86匯編語言中我認為需要注意的重要概念。許多東西,包括全部匯編語句的時序特性(指令執(zhí)行周期數(shù),以及指令周期中各個階段的節(jié)拍數(shù)等)、功能、參數(shù)等等,限于個人水平和篇幅我都沒有作詳細介紹。如果您對這些內(nèi)容感興趣,請參考Intel和AMD兩大CPU供應(yīng)商網(wǎng)站上提供的開發(fā)人員參考。

      在以后的簡明x86匯編語言教程中級篇和高級篇中,我將著重介紹匯編語言的調(diào)試技術(shù)、優(yōu)化,以及一些具體的應(yīng)用技巧,包括反跟蹤、反反跟蹤、加密解密、病毒與反病毒等等。

      5.0 編譯優(yōu)化概述

      優(yōu)化是一件非常重要的事情。作為一個程序設(shè)計者,你肯定希望自己的程序既小又快。DOS時代的許多書中都提到,“某某編譯器能夠生成非常緊湊的代碼”,換言之,編譯器會為你把代碼盡可能地縮減,如果你能夠正確地使用它提供的功能的話。目前,Intel x86體系上流行的C/C++編譯器,包括Intel C/C++ Compiler, GNU C/C++ Compiler,以及最新的Microsoft和Borland編譯器,都能夠提供非常緊湊的代碼。正確地使用這些編譯器,則可以得到性能足夠好的代碼。

      但是,機器目前還不能像人那樣做富于創(chuàng)造性的事情。因而,有些時候我們可能會不得不手工來做一些事情。

      使用匯編語言優(yōu)化代碼是一件困難,而且技巧性很強的工作。很多編譯器能夠生成為處理器進行過特殊優(yōu)化處理的代碼,一旦進行修改,這些特殊優(yōu)化可能就會被破壞而失效。因此,在你決定使用自己的匯編代碼之前,一定要測試一下,到底是編譯器生成的那段代碼更好,還是你的更好。

      本章中將討論一些編譯器在某些時候會做的事情(從某種意義上說,本章內(nèi)容更像是計算機專業(yè)的基礎(chǔ)課中《編譯程序設(shè)計原理》、《計算機組成原理》、《計算機體系結(jié)構(gòu)》課程中的相關(guān)內(nèi)容)。本章的許多內(nèi)容和匯編語言程序設(shè)計本身關(guān)系并不是很緊密,它們多數(shù)是在為使用匯編語言進行優(yōu)化做準備。編譯器確實做這些優(yōu)化,但它并不總是這么做;此外,就編譯器的設(shè)計本質(zhì)來說,它確實沒有義務(wù)這么做——編譯器做的是等義變換,而不是等效變換??紤]下面的代碼:

      // 程序段1 int gaussianSum(){ int i, j=0;for(i=0;i<100;i++)j+=i;

      return j;} 好的,首先,絕大多數(shù)編譯器恐怕不會自作主張地把它“篡改”為

      // 程序段1(改進1)int gaussianSum(){ int i, j=0;for(i=1;i<100;i++)j+=i;

      return j;}

      多數(shù)(但確實不是全部)編譯器也不會把它改為

      // 程序段1(改進2)inline int gaussianSum(){ return 5050;}

      這兩個修改版本都不同于原先程序的語義。首先我們看到,讓i從0開始是沒有必要的,因為j+=i時,i=0不會做任何有用的事情;然后是,實際上沒有必要每一次都計算1+...+100的和——它可以被預(yù)先計算,并在需要的時候返回。

      這個例子也許并不恰當(估計沒人會寫出最初版本那樣的代碼),但這種實踐在程序設(shè)計中確實可能出現(xiàn)。我們把改進2稱為編譯時表達式預(yù)先計算,而把改進1成為循環(huán)強度削減。

      然而,一些新的編譯器的確會進行這兩種優(yōu)化。不過別慌,看看下面的代碼:

      // 程序段2 int GetFactorial(int k){ int i, j=1;if((k<0)||(k>=10))return-1;

      if((k<=1))return 1

      for(i=1;i

      return j;}

      程序采用的是一個時間復(fù)雜度為O(n)的算法,不過,我們可以把他輕易地改為O(1)的算法:

      // 程序段2(非規(guī)范改進)int GetFactorial(int k){ int i, j=1;static const int FractorialTable[]={1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800};

      if((k<0)||(k>=10))return-1;

      return FractorialTable[k];}

      這是一個典型的以空間換時間的做法。通用的編譯器不會這么做——因為它沒有辦法在編譯時確定你是不是要這么改??梢哉f,如果編譯器真的這樣做的話,那將是一件可怕的事情,因為那時候你將很難知道編譯器生成的代碼和自己想的到底有多大的差距。

      當然,這類優(yōu)化超出了本文的范圍——基本上,我把它們歸入“算法優(yōu)化”,而不是“程序優(yōu)化”一類。類似的優(yōu)化過程需要程序設(shè)計人員對于程序邏輯非常深入地了解和全盤的掌握,同時,也需要有豐富的算法知識。

      自然,如果你希望自己的程序性能有大幅度的提升,那么首先應(yīng)該做的是算法優(yōu)化。例如,把一個O(n2)的算法替換為一個O(n)的算法,則程序的性能提升將遠遠超過對于個別語句的修改。此外,一個已經(jīng)改寫為匯編語言的程序,如果要再在算法上作大幅度的修改,其工作量將和重寫相當。因此,在決定使用匯編語言進行優(yōu)化之前,必須首先考慮算法優(yōu)化。但假如已經(jīng)是最優(yōu)的算法,程序運行速度還是不夠快怎么辦呢?

      好的,現(xiàn)在,假定你已經(jīng)使用了已知最好的算法,決定把它交給編譯器,讓我們來看看編譯器會為我們做什么,以及我們是否有機會插手此事,做得更好。

      5.1 循環(huán)優(yōu)化:強度削減和代碼外提

      比較新的編譯器在編譯時會自動把下面的代碼:

      for(i=0;i<10;i++){ j = i;k = j + i;}

      至少變換為

      for(i=0;i<10;i++);j=i;k=j+i;

      甚至

      j=i=10;k=20;

      當然,真正的編譯器實際上是在中間代碼層次作這件事情。

      原理 如果數(shù)據(jù)項的某個中間值(程序執(zhí)行過程中的計算結(jié)果)在使用之前被另一中間值覆蓋,則相關(guān)計算不必進行。

      也許有人會問,編譯器不是都給咱們做了嗎,管它做什么?注意,這里說的只是編譯系統(tǒng)中優(yōu)化部分的基本設(shè)計。不僅在從源代碼到中間代碼的過程中存在優(yōu)化問題,而且編譯器生成的最終的機器語言(匯編)代碼同樣存在類似的問題。目前,幾乎所有的編譯器在最終生成代碼的過程中都有或多或少的瑕疵,這些瑕疵目前只能依靠手工修改代碼來解決。

      5.2 局部優(yōu)化:表達式預(yù)計算和子表達式提取

      表達式預(yù)先計算非常簡單,就是在編譯時盡可能地計算程序中需要計算的東西。例如,你可以毫不猶豫地寫出下面的代碼:

      const unsigned long nGiga = 1024L * 1024L * 1024L;

      而不必擔(dān)心程序每次執(zhí)行這個語句時作兩遍乘法,因為編譯器會自動地把它改為

      const unsigned long nGiga = 1073741824L;

      而不是傻乎乎地讓計算機在執(zhí)行到這個初始化賦值語句的時候才計算。當然,如果你愿意在上面的代碼中摻上一些變量的話,編譯器同樣會把常數(shù)部分先行計算,并拿到結(jié)果。

      表達式預(yù)計算并不會讓程序性能有飛躍性的提升,但確實減少了運行時的計算強度。除此之外,絕大多數(shù)編譯器會把下面的代碼:

      // [假設(shè)此時b, c, d, e, f, g, h都有一個確定的非零整數(shù)值,并且,// a[]為一個包括5個整數(shù)元素的數(shù)組,其下標為0到4] a[0] = b*c;a[1] = b+c;a[2] = d*e;a[3] = b*d + c*d;a[4] = b*d*e + c*d*e;

      優(yōu)化為(再次強調(diào),編譯器實際上是在中間代碼的層次,而不是源代碼層次做這件事情!):

      // [假設(shè)此時b, c, d, e, f, g, h都有一個確定的非零整數(shù)值,并且,// a[]為一個包括5個整數(shù)元素的數(shù)組,其下標為0到4] a[0] = b*c;a[1] = b+c;a[2] = d*e;a[3] = a[1] * d;a[4] = a[3] * e;

      更進一步,在實際代碼生成過程中,一些編譯器還會對上述語句的次序進行調(diào)整,以使其運行效率更高。例如,將語句調(diào)整為下面的次序:

      // [假設(shè)此時b, c, d, e, f, g, h都有一個確定的非零整數(shù)值,并且,// a[]為一個包括5個整數(shù)元素的數(shù)組,其下標為0到4] a[0] = b*c;a[1] = b+c;a[3] = a[1] * d;a[4] = a[3] * e;a[2] = d*e;

      在某些體系結(jié)構(gòu)中,剛剛計算完的a[1]可以放到寄存器中,以提高實際的計算性能。上述5個計算任務(wù)之間,只有1, 3, 4三個計算任務(wù)必須串行地執(zhí)行,因此,在新的處理器上,這樣做甚至能夠提高程序的并行度,從而使程序效率變得更高。

      5.3 全局寄存器優(yōu)化

      [待修訂內(nèi)容] 本章中,從這一節(jié)開始的所有優(yōu)化都是在微觀層面上的優(yōu)化了。換言之,這些優(yōu)化是不能使用高級語言中的對應(yīng)設(shè)施進行解釋的。這一部分內(nèi)容將進行較大規(guī)模的修訂。

      通常,此類優(yōu)化是由編譯器自動完成的。我個人并不推薦真的由人來完成這些工作——這些工作多半是枯燥而重復(fù)性的,編譯器通常會比人做得更好(沒說的,肯定也更快)。但話說回來,使用匯編語言的程序設(shè)計人員有責(zé)任了解這些內(nèi)容,因為只有這樣才能更好地駕馭處理器。

      在前面的幾章中我已經(jīng)提到過,寄存器的速度要比內(nèi)存快。因此,在使用寄存器方面,編譯器一般會做一種稱為全局寄存器優(yōu)化的優(yōu)化。

      例如,在我們的程序中使用了4個變量:i, j, k, l。它們都作為循環(huán)變量使用:

      for(i=0;i<1000;i++){ for(j=0;j<1000;j++){ for(k=0;k<1000;k++){ for(l=0;l<1000;l++)do_something(i, j, k, l);} } }

      這段程序的優(yōu)化就不那么簡單了。顯然,按照通常的壓棧方法,i, j, k, l應(yīng)該按照某個順序被壓進堆棧,然后調(diào)用do_something(),然后函數(shù)做了一些事情之后返回。問題在于,無論如何壓棧,這些東西大概都得進內(nèi)存(不可否認某些機器可以用CPU的Cache做這件事情,但Cache是寫通式的和回寫式的又會造成一些性能上的差異)。

      聰明的讀者馬上就會指出,我們不是可以在定義do_something()的時候加上inline修飾符,讓它在本地展開嗎?沒錯,本地展開以增加代碼量為代價換取性能,但這只是問題的一半。編譯器盡管完成了本地展開,但它仍然需要做許多額外的工作。因為寄存器只有那么有限的幾個,而我們卻有這么多的循環(huán)變量。

      把四個變量按照它們在循環(huán)中使用的頻率排序,并決定在do_something()塊中的優(yōu)先順序(放入寄存器中的優(yōu)先順序)是一個解決方案。很明顯,我們可以按照l, k, j, i的順序(從高到低,因為l將被進行1000*1000*1000*1000次運算!)來排列,但在實際的問題中,事情往往沒有這么簡單,因為你不知道do_something()中做的到底是什么。而且,憑什么就以for(l=0;l<1000;l++)作為優(yōu)化的分界點呢?如果do_something()中還有循環(huán)怎么辦?

      如此復(fù)雜的計算問題交給計算機來做通常會有比較滿意的結(jié)果。一般說來,編譯器能夠?qū)Τ绦蛑凶兞康氖褂眠M行更全面地估計,因此,它分配寄存器的結(jié)果有時雖然讓人費解,但卻是最優(yōu)的(因為計算機能夠進行大量的重復(fù)計算,并找到最好的方法;而人做這件事相對來講比較困難)。

      編譯器在許多時候能夠作出相當讓人滿意的結(jié)果??紤]以下的代碼:

      int a=0;for(int i=1;i<10;i++)for(int j=1;j<100;j++){ a +=(i*j);}

      讓我們把它變?yōu)槟撤N形式的中間代碼:

      00: 0-> a 01: 1-> i 02: 1-> j 03: i*j-> t 04: a+t-> a 05: j+1-> j 06: evaluate j < 100 07: TRUE? goto 03 08: i+1-> i 09: evaluate i < 10 10: TRUE? goto 02 11: [繼續(xù)執(zhí)行程序的其余部分]

      程序中執(zhí)行強度最大的無疑是03到05這一段,涉及的需要寫入的變量包括a, j;需要讀出的變量是i。不過,最終的編譯結(jié)果大大出乎我們的意料。下面是某種優(yōu)化模式下Visual C++ 6.0編譯器生成的代碼(我做了一些修改):

      xor eax, eax;a=0(eax: a)mov edx, 1;i=1(edx: i)push esi;保存esi(最后要恢復(fù),esi作為代替j的那個循環(huán)變量)nexti: mov ecx, edx;[t=i] mov esi, 999;esi=999: 此處修改了原程序的語義,但仍為1000次循環(huán)。nextj: add eax, ecx;[a+=t] add ecx, edx;[t+=i] dec esi;j--jne SHORT nextj;jne 等價于 jnz.[如果還需要,則再次循環(huán)] inc edx;i++ cmp edx, 10;i與10比較 jl SHORT nexti;i < 10, 再次循環(huán) pop esi;恢復(fù)esi

      這段代碼可能有些令人費解。主要是因為它不僅使用了大量寄存器,而且還包括了5.2節(jié)中曾提到的子表達式提取技術(shù)。表面上看,多引入的那個變量(t)增加了計算時間,但要注意,這個t不僅不會降低程序的執(zhí)行效率,相反還會讓它變得更快!因為同樣得到了計算結(jié)果(本質(zhì)上,i*j即是第j次累加i的值),但這個結(jié)果不僅用到了上次運算的結(jié)果,而且還省去了乘法(很顯然計算機計算加法要比計算乘法快)。

      這里可能會有人問,為什么要從999循環(huán)到0,而不是按照程序中寫的那樣從0循環(huán)到999呢?這個問題和匯編語言中的取址有關(guān)。在下兩節(jié)中我將提到這方面的內(nèi)容。

      5.4 x86體系結(jié)構(gòu)上的并行最大化和指令封包

      考慮這樣的問題,我和兩個同伴現(xiàn)在在山里,遠處有一口井,我們帶著一口鍋,身邊是樹林;身上的飲用水已經(jīng)喝光了,此處允許砍柴和使用明火(當然我們不想引起火災(zāi):),需要燒一鍋水,應(yīng)該怎么樣呢?

      一種方案是,三個人一起搭灶,一起砍柴,一起打水,一起把水燒開。

      另一種方案是,一個人搭灶,此時另一個人去砍柴,第三個人打水,然后把水燒開。

      這兩種方案畫出圖來是這樣:

      僅僅這樣很難說明兩個方案孰優(yōu)孰劣,因為我們并不明確三個人一起打水、一起砍柴、一起搭灶的效率更高,還是分別作效率更高(通常的想法,一起做也許效率會更高)。但假如說,三個人一個只會搭灶,一個只會砍柴,一個只會打水(當然是說這三件事情),那么,方案2的效率就會搞一些了。

      在現(xiàn)實生活中,某個人擁有專長是比較普遍的情況;在設(shè)計計算機硬件的時候則更是如此。你不可能指望加法器不做任何改動就能去做移位甚至整數(shù)乘法,然而我們注意到,串行執(zhí)行的程序不可能在同一時刻同時用到處理器的所有功能,因此,我們(很自然地)會希望有一些指令并行地執(zhí)行,以充分利用CPU的計算資源。

      CPU執(zhí)行一條指令的過程基本上可以分為下面幾個階段:取指令、取數(shù)據(jù)、計算、保存數(shù)據(jù)。假設(shè)這4個階段各需要1個時鐘周期,那么,只要資源夠用,并且4條指令之間不存在串行關(guān)系(換言之這些指令的執(zhí)行先后次序不影響最終結(jié)果,或者,更嚴格地說,沒有任何一條指令依賴其他指令的運算結(jié)果)指令也可以像下面這樣執(zhí)行:

      指令1 取指令 取數(shù)據(jù) 計 算 存數(shù)據(jù)

      指令2 取指令 取數(shù)據(jù) 計 算 存數(shù)據(jù)

      指令3

      取指令 取數(shù)據(jù) 計 算 存數(shù)據(jù)

      指令4

      取指令 取數(shù)據(jù) 計 算 存數(shù)據(jù)

      這樣,原本需要16個時鐘周期才能夠完成的任務(wù)就可以在7個時鐘周期內(nèi)完成,時間縮短了一半還多。如果考慮灰色的那些方格(這些方格可以被4條指令以外的其他指令使用,只要沒有串行關(guān)系或沖突),那么,如此執(zhí)行對于性能的提升將是相當可觀的(此時,CPU的所有部件都得到了充分利用)。

      當然,作為程序來說,真正做到這樣是相當理想化的情況。實際的程序中很難做到徹底的并行化。假設(shè)CPU能夠支持4條指令同時執(zhí)行,并且,每條指令都是等周期長度的4周期指令,那么,程序需要保證同一時刻先后發(fā)射的4條指令都能夠并行執(zhí)行,相互之間沒有關(guān)聯(lián),這通常是不太可能的。

      最新的Intel Pentium 4-XEON處理器,以及Intel Northwood Pentium 4都提供了一種被稱為超線程(Hyper-Threading TM)的技術(shù)。該技術(shù)通過在一個處理器中封裝兩組執(zhí)行機構(gòu)來提高指令并行度,并依靠操作系統(tǒng)的調(diào)度來進一步提升系統(tǒng)的整體效率。

      由于線程機制是與操作系統(tǒng)密切相關(guān)的,因此,在本文的這一部分中不可能做更為深入地探討。在后續(xù)的章節(jié)中,我將介紹Win32、FreeBSD 5.x以及Linux中提供的內(nèi)核級線程機制(這三種操作系統(tǒng)都支持SMP及超線程技術(shù),并且以線程作為調(diào)度單位)在匯編語言中的使用方法。

      關(guān)于線程的討論就此打住,因為它更多地依賴于操作系統(tǒng),并且,無論如何,操作系統(tǒng)的線程調(diào)度需要更大的開銷并且,到目前為止,真正使用支持超線程的CPU,并且使用相應(yīng)操作系統(tǒng)的人是非常少的。因此,我們需要關(guān)心的實際上還是同一執(zhí)行序列中的并發(fā)執(zhí)行和指令封包。不過,令人遺憾的是,實際上在這方面編譯器做的幾乎是肯定要比人好,因此,你需要做的只是開啟相應(yīng)的優(yōu)化;如果你的編譯器不支持這樣的特性,那么就把它扔掉??據(jù)我所知,目前在Intel平臺上指令封包方面做的最好的是Intel的C++編譯器,經(jīng)過Intel編譯器編譯的代碼的性能令人驚異地高,甚至在AMD公司推出的兼容處理器上也是如此。5.5 存儲優(yōu)化

      從前一節(jié)的圖中我們不難看出,方案2中,如果誰的動作慢,那么他就會成為性能的瓶頸。實際上,CPU也不會像我描述的那樣四平八穩(wěn)地運行,指令執(zhí)行的不同階段需要的時間(時鐘周期數(shù))是不同的,因此,縮短關(guān)鍵步驟(即,造成瓶頸的那個步驟)是縮短執(zhí)行時間的關(guān)鍵。

      至少對于使用Intel系列的CPU來說,取數(shù)據(jù)這個步驟需要消耗比較多的時間。此外,假如數(shù)據(jù)跨越了某種邊界(如4或8字節(jié),與CPU的字長有關(guān)),則CPU需要啟動兩次甚至更多次數(shù)的讀內(nèi)存操作,這無疑對性能構(gòu)成不利影響。

      基于這樣的原因,我們可以得到下面的設(shè)計策略:

      程序設(shè)計中的內(nèi)存數(shù)據(jù)訪問策略

      盡可能減少對于內(nèi)存的訪問。在不違背這一原則的前提下,如果可能,將數(shù)據(jù)一次處理完。盡可能將數(shù)據(jù)按4或8字節(jié)對齊,以利于CPU存取

      盡可能一段時間內(nèi)訪問范圍不大的一段內(nèi)存,而不同時訪問大量遠距離的分散數(shù)據(jù),以利于Cache緩存*

      第一條規(guī)則比較簡單。例如,需要求一組數(shù)據(jù)中的最大值、最小值、平均數(shù),那么,最好是在一次循環(huán)中做完。

      “于是,這家伙又攢了一段代碼”??

      int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};int i;int avg, max, min;avg=max=min=a[0];

      for(i=1;i<(sizeof(a)/sizeof(int));i++){ avg+=a[i];if(max < a[i])max = a[i];else if(min > a[i])min = a[i];}

      avg /= i;

      Visual C++編譯器把最開始一段賦值語句翻譯成了一段簡直可以說是匪夷所思的代碼:

      ;int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};

      mov edi, 2;此時edi沒有意義

      mov esi, 3;esi也是!臨時變量而已。mov DWORD PTR _a$[esp+92], edi mov edx, 5;黑名單加上edx mov eax, 7;eax也別跑:)mov DWORD PTR _a$[esp+132], edi mov ecx, 9;就差你了,ecx

      ;int i;;int avg, max, min;;avg=max=min=a[0];

      mov edi, 1;edi搖身一變,現(xiàn)在它是min了。mov DWORD PTR _a$[esp+96], esi mov DWORD PTR _a$[esp+104], edx mov DWORD PTR _a$[esp+112], eax mov DWORD PTR _a$[esp+136], esi mov DWORD PTR _a$[esp+144], edx mov DWORD PTR _a$[esp+152], eax mov DWORD PTR _a$[esp+88], 1;編譯器失誤? 此處edi應(yīng)更好 mov DWORD PTR _a$[esp+100], 4 mov DWORD PTR _a$[esp+108], 6 mov DWORD PTR _a$[esp+116], 8 mov DWORD PTR _a$[esp+120], ecx mov DWORD PTR _a$[esp+124], 0 mov DWORD PTR _a$[esp+128], 1 mov DWORD PTR _a$[esp+140], 4 mov DWORD PTR _a$[esp+148], 6 mov DWORD PTR _a$[esp+156], 8 mov DWORD PTR _a$[esp+160], ecx mov DWORD PTR _a$[esp+164], 0 mov edx, edi;edx是max。

      mov eax, edi;期待已久的avg, 它被指定為eax

      這段代碼是最優(yōu)的嗎?我個人認為不是。因為編譯器完全可以在編譯過程中直接把它們作為常量數(shù)據(jù)放入內(nèi)存。此外,如果預(yù)先對a[0..9]10個元素賦值,并利用串操作指令(rep movsdw),速度會更快一些。

      當然,犯不上因為這些問題責(zé)怪編譯器。要求編譯器知道a[0..9]和[10..19]的內(nèi)容一樣未免過于苛刻。我們看看下面的指令段:

      ;for(i=1;...mov esi, edi for_loop:

      ;avg+=a[i];

      mov ecx, DWORD PTR _a$[esp+esi*4+88] add eax, ecx

      ;if(max < a[i])

      cmp edx, ecx jge SHORT elseif_min

      ;max = a[i];

      mov edx, ecx

      ;else if(min > a[i])

      jmp SHORT elseif_min elseif_min: cmp edi, ecx jle SHORT elseif_end

      ;min = a[i];mov edi, ecx

      elseif_end:

      ;[for i=1];i<20;i++){

      inc esi cmp esi, 20 jl SHORT for_loop;};avg /= i;cdq idiv esi

      ;esi: i

      ;ecx: 暫存變量, =a[i];eax: avg

      ;edx: max

      ;有趣的代碼...并不是所有的時候都有用;但是也別隨便刪除;edi: min

      ;i++;i與20比較

      ;avg /= i

      上面的程序倒是沒有什么驚人之處。唯一一個比較嚇人的東西是那個jmp SHORT指令,它是否有用取決于具體的問題。C/C++編譯器有時會產(chǎn)生這樣的代碼,我過去曾經(jīng)錯誤地把所有的此類指令當作沒用的代碼而刪掉,后來發(fā)現(xiàn)程序執(zhí)行時間沒有明顯的變化。通過查閱文檔才知道,這類指令實際上是“占位指令”,他們存在的意義在于占據(jù)那個地方,一來使其他語句能夠正確地按CPU覺得舒服的方式對齊,二來它可以占據(jù)CPU的某些周期,使得后續(xù)的指令能夠更好地并發(fā)執(zhí)行,避免沖突。另一個比較常見的、實現(xiàn)類似功能的指令是NOP。

      占位指令的去留主要是靠計時執(zhí)行來判斷。由于目前流行的操作系統(tǒng)基本上都是多任務(wù)的,因此會對計時的精確性有一定影響。如果需要進行測試的話,需要保證以下幾點:

      計時測試需要注意的問題

      測試必須在沒有額外負荷的機器上完成。例如,專門用于編寫和調(diào)試程序的計算機 盡量終止計算機上運行的所有服務(wù),特別是殺毒程序 切斷計算機的網(wǎng)絡(luò),這樣網(wǎng)絡(luò)的影響會消失

      將進程優(yōu)先級調(diào)高。對于Windows系統(tǒng)來說,把進程(線程)設(shè)置為Time-Critical;對于*nix系統(tǒng)來說,把進程設(shè)置為實時進程

      將測試函數(shù)運行盡可能多次運行,如10000000次,這樣能夠減少由于進城切換而造成的偶然誤差

      最后,如果可能的話,把函數(shù)放到單進程的系統(tǒng)(例如FreeDOS)中運行。

      對于絕大多數(shù)程序來說,計時測試是一個非常重要的東西。我個人傾向于在進行優(yōu)化后進行計時測試并比較結(jié)果。目前,我基于經(jīng)驗進行的優(yōu)化基本上都能夠提高程序的執(zhí)行性能,但我還是不敢過于自信。優(yōu)化確實會提高性能,但人做的和編譯器做的思路不同,有時,我們的確會做一些費力不討好的事情。

      下載C語言書籍推薦(范文模版)word格式文檔
      下載C語言書籍推薦(范文模版).doc
      將本文檔下載到自己電腦,方便修改和收藏,請勿使用迅雷等下載。
      點此處下載文檔

      文檔為doc格式


      聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻自行上傳,本網(wǎng)站不擁有所有權(quán),未作人工編輯處理,也不承擔(dān)相關(guān)法律責(zé)任。如果您發(fā)現(xiàn)有涉嫌版權(quán)的內(nèi)容,歡迎發(fā)送郵件至:645879355@qq.com 進行舉報,并提供相關(guān)證據(jù),工作人員會在5個工作日內(nèi)聯(lián)系你,一經(jīng)查實,本站將立刻刪除涉嫌侵權(quán)內(nèi)容。

      相關(guān)范文推薦

        書籍

        書籍,你是我的四季友伴時光穿梭,歲月流逝。轉(zhuǎn)眼之間,16個春秋已過,在人生長途上留下了深邃的記憶。在這人生的四季中,唯有書籍的相伴,使我不孤單,令我的生活煥然一新,豐富多姿。 書......

        書籍推薦

        公司副總推薦的書單,我開始膜拜他了 來源: 陳欣興的日志 各位同事: 最近大家都有購書的需求,我結(jié)合最近收集到的信息推薦一些好書給大家,這其中的少部分書我看過,很是不錯,所以......

        推薦書籍

        商業(yè)興衰 《1929 年大崩盤》(The Great Crash 1929),加爾布雷思(John Kenneth Galbraith)著,1955 年出版。這是本簡明扼要但又富有見地的歷史著作,初版以來一直在重印。原因何在?加......

        書籍

        市趨勢技術(shù)分析》下載 《日本蠟燭圖技術(shù)》下載 《股票K線戰(zhàn)法》下載 《證券混沌操作法-低風(fēng)險獲利指南》下載《證券交易新空間》下載 《亞當理論》下載 《股市晴雨表》下載......

        推薦書籍

        推薦書籍 營銷 《市場營銷3.0》; 管理 《管理大未來》;《管理十律》;《力量》;《第五項修煉》; 《贏》;《史蒂夫·喬布斯管理日志》;《麥肯錫方法》;《管理的實踐》 《只有偏執(zhí)狂才......

        書籍推薦

        推薦書目 A系列 《從管理新人到卓越管理者》 《世界35位頂級企業(yè)家的經(jīng)營之道》 《德魯克的管理課-百年經(jīng)典十五講》 《高盛陰謀》《高盛在中國》 《私營公司的10堂細節(jié)防敗......

        書籍整理

        十萬斤為什么(2012.11) 牛津劍橋的59道思考題(2012.2) 大二(2) 囧問題(2013.3) 大腦潛能與開發(fā)(2013.3) 哈佛精英 怎樣培養(yǎng)未來的精英 教育全書(2013.3) 思考致富 拿破侖希爾寫給男人的3......

        書籍推薦

        江山房產(chǎn)公司很多哦,大點的 有恒泰房產(chǎn),華達房產(chǎn),國鑫房產(chǎn),祥華房產(chǎn),貝林房產(chǎn),長河房產(chǎn),小點的有同泰房產(chǎn),金和龍房產(chǎn),萬盛房產(chǎn)等等 浙江省江山市江濱路29號(浙江省江山市八方旅游公......