高級語言寫代碼時就能夠想到對應的彙編代碼是怎樣一種體驗?
從事編譯器工作的話能做到「高級語言寫代碼時就能夠想到對應的彙編代碼」是頗為平常的事吧。術業專攻。
相信很多同行對自己維護的編譯器都能做到這點,並不是什麼「很厲害」的事情,實用倒是挺實用的。畢竟,連編譯器的開發者都摸不清自己的編譯器生成的代碼長啥樣的話,那還能指望用戶用得放心么。當然,專門做逆向工程的人也會因為需要而能熟練做到這點。不然像heap spray之類這麼好玩的東西誰來弄 &>_&<我寫Java的時候,從Java源碼到位元組碼,我人肉編譯的結果可以跟javac生成的完全一致。這樣我在寫特定的測試時可以寫Java代碼就能構造出我需要的位元組碼,而很少要直接手寫位元組碼(偶爾還是要的…)。Java源碼與位元組碼之間的雙向映射頗為直觀,有興趣可跳傳送門:如何理解ByteCode、IL、彙編等底層語言與上層語言的對應關係? - RednaxelaFX 的回答
然後從位元組碼到機器碼,我就會預期著我參與維護的JIT編譯器會生成出怎樣的代碼模式,然後在檢查實際生成的代碼時如果跟我預期的不同,我就得去看看哪裡出錯了,或者說是不是有啥優化漏了沒做嗯。
同時,從我們的JIT編譯器生成的機器碼反推出源碼也是我經常要做的事情。經常要解些客戶遇到的bug,手上就只有一個core dump file / bundle,其它啥都沒了,客戶也不會共享源碼給我們,那就只能人肉從生成的機器碼來分析出程序原本應該在做什麼,而哪裡看起來出錯了。
對別的編譯器我當然做不到對自己維護的編譯器那麼熟悉,不過有些東西還是有規可循的。
例如這樣:Visual C++ 6以debug模式編譯很拙笨,為何要做無用功? - RednaxelaFX 的回答
或者這樣:提出結論,給出論據(二) &<- 嗯看C#我也是能大致估摸出會得到怎樣的機器碼的。不然怎麼玩這種把戲:要讓CLR掛掉的話(第二彈)……是的我沒有拋棄微軟 &>_&<然後像腳本語言之類的,要在心中對執行了怎樣的機器碼有數當然也是做得到的。
例如之前玩Ruby的時候:beef - Script Ahead, Code Behind在Windows上使用Wilson有別的回答提到CSAPP。正確閱讀了它的話確實應該能掌握不少基礎知識,例如這樣:編譯器生成的彙編語句執行順序為什麼與C代碼順序不同? - RednaxelaFX 的回答
如果是debug build,這太尋常了,很多人都能做到,也沒什麼特別體驗。就算是constant propagation,escape analysis這些簡單(雖然可能不完美)優化,也是很容易想像出來的。
做編譯器的不一樣,多多少少能看到release build後面的彙編長什麼樣子。但是幾乎不可能全看透。
比如我看到個小函數想想多半會被inliner幹掉了。但是多小才會被幹掉呢?嘿嘿。。
看到個小循環想想應該會有vectorization。llvm還有個叫interleave的技術。很多循環技術背後用的是一套叫Scalar Evolution的小理論來分析循環不變數。編譯器自己不會告訴你stride大於等於2的時候才會有interleave,或者會不會還順帶幫我做個prefetch。
如果有pgo的話,我會想能不能幫我帶點expected的branching。pgo我同事在做,我是完全不了解。
像LLVM還有兩趟instruction scheduling,會對指令進行重新排序。像C++現在還明確規定了不同的memory orders。怎麼排還得看實現。
看到一個大函數就很不確定了,如果局部變數太多寄存器不夠用就得考慮什麼時候range split + spill,這時候讀內存的指令插哪兒不了解寄存器分配的演算法實現是沒數的。
冷知識:powerpc上tls變數讀取需要一個函數調用。
編譯器太多黑科技了。就算背後的彙編能看個通透,那也只能說明很了解這個編譯器而已,感覺蠻雞肋的。最不能忍的是,編譯器(主要是較老的版本)不能生成我想要的指令,多了一些無意義的垃圾。之前做 SIMD 庫的時候用 intrinsics 常會出現,要用各種 hacks 去調教編譯器。
我曾說過,腳本語言如JavaScript並不適合學習計算機科學,因為中間有太多黑盒,不能直覺地知道代碼到底會怎樣執行。C語言表示,本來就是跟彙編有對應關係的,愛怎麼對應怎麼對應。
Python表示,主流解釋器是C寫的,會發生什麼都可以預料,然而考慮這個就沒必要python了……
彙編表示,你95%的時間不需要鳥我是啥,先關注自身語言邏輯,剩下5%如果非看不可,要麼是折騰編譯器的,要麼是語言特性沒學好的,要麼是語言太新編譯出bug了……以前自學過 x86 的彙編。現在具體的指令很多都忘記了,但是機器架構還是熟悉的。
之後就很容易將 C 的結構、函數調用、全局變數、局部變數、棧內存、循環、分支等語句跟彙編對應起來。用 C++ 後,了解了 C++ 的對象模型,也會很容易將 C++ 的類、成員函數、虛函數等等跟 C 語句對應起來。用 Objectiv-C 後,了解了 Objectiv-C 的對象模型,也很自然將 Objectiv-C 跟 C 語句對應起來。
這樣寫代碼的時候,會很大致判斷出每一行代碼的代價。另外會覺得程序並非是靜態的,每行代碼流動起來。這樣心裡會更踏實。
其實這樣還有個好處,會記得自己在項目中寫過的代碼。有些人會重複代碼,有個原因是他忘記寫過類似的代碼了。
上面我說的只是 x86 的彙編,至於 ARM 的彙編其實我一點都不了解。現在又切換到 Swift 了,但還不了解 Swift 的結構、類、函數調用等的抽象代價,總覺得有點不踏實。C/C++程序員一定要有考慮彙編、堆棧狀態、符號表、編譯器的能力。在離開學校開始求職的那一刻,如果尚不能做到這一點,那麼要選擇走C/C++這條路難度大大提升。如果自己寫過編譯器,哪怕是最簡單的玩具型編譯器,會有極大幫助。不然,沒必要自找麻煩用C/C++。
如果寫程序的時候心裡明了堆棧狀態、符號表狀態、編譯器狀態,其實不會彙編也沒什麼。可惜,如果一個人不會彙編,要理解這三者實在是太難了。反過來,如果理解了這三者,基本上邊寫代碼邊人肉轉彙編都是自然而然的事情。
- 不考慮符號表的,容易出Linker Error且看不懂Linker Errorr,容易分不清什麼時候該include什麼時候forward declare就行,對function signature、template、inline理解肯定也不會很到位。
- 不考慮堆棧狀態的,看到指針就發怵,用不好Allocator,動態分配時分不清應該直接從堆上動態分配好,還是從棧上開buffer分配內存,會把棧上變數的指針bind給async callback函數。
- 不考慮編譯器的,容易寫出帶二義性的代碼,面對GCC的錯誤一半時間都得抓瞎,分不清runtime和compile time,於是template meta programming啥的也就別談了。
- 不考慮彙編的,虛函數和非虛函數的繼承,vptr,信號和中斷處理函數一些詭異的特性,轉型中的內存截斷,強制類型轉換,指針,線程切換,保存現場、上下文恢復也會理解不到位。
至於其他語言,人肉彙編不是必須的技能。畢竟,那些高級語言,存在的目的就是為了對底層進行抽象。如果對彙編有所了解,那很好,某個語言的某一個獨特feature為什麼這麼設計,支持這個特性的代價是什麼,會一目了然。
而C/C++,在我看來是特殊的,這兩個語言(在我看來)完全是為了「更快更好地 用現代軟體工程的方法 寫出接近彙編的 細粒度 高性能代碼」 而存在的。所以,C/C++程序員必須通曉彙編看完csapp第三章時候的體驗。
什麼,難道你們編程的時候做不到這點嗎?
目前只有寫C會有,其他的語言不會。因為之前學習C語言時候,有個章節讓你彙編和C語言翻譯的,觀察參數傳遞過程。還有一個就是觀察編譯器對指令的優化,gcc優化選項開的高,會把一些值變成常量。後來學習一些東西,想得還會想的多一點,比如棧空間的分布。
-S多了就記住了。
炸出了一堆真正的senior啊!這是知乎炸魚最好的一次了,給個贊!
出問題經常會腦內debug……
程序不跑就知道哪兒錯了曾經寫過CPU 32位寄存器操作數據的圖形渲染庫,然後發現VC生成的彙編代碼,比如循環,判斷語句會有很多為了兼容性而出現的 讀-寫-讀-寫,而我直接操作寄存器一個mov就能解決,以後寫代碼的時候看到for循環就會有種厭惡的感覺。
腦海中總是各種箭頭和內存塊。
我們這種看到一條語句就能推導出出cpu和內存匯流排上波形的咋辦呢?
寫代碼時能夠預見到編譯器會生成什麼樣的對應彙編代碼是一個優秀程序員必須做到的。不是什麼奇技淫巧。
雖然好多大神回答地不亦樂乎,但我還是想說,大部分流行的高級語言沒發直接腦補成彙編。。。
這就好比開自動檔的汽車時候腦補手動檔的運作方式,挺沒意思的。
不容易 前兩天看一段很簡單的彙編 差點以為編譯器出錯了
C代碼是這樣的 if (char_x == "e" || char_x == "E") .....粗一看怎麼只比較了一次 比較的是該變數和"e"彙編難么?彙編只是麻煩,它的規則比C還少,只是給你那麼幾個寄存器然後讓你用有限的資源去完成一些事情。
推薦閱讀:
※如何學習JIT,能提供一些系統全面的路線和材料嗎?
※想編寫一個虎書中的編譯器,該如何上手?
※早就聽聞編譯原理很難,而且很重要,寫一個編譯器算是合格,快放假了,寒假大概40天左右,能學個什麼程度?
※如何寫一個簡單的編譯器?