為什麼有些語言可以被反編譯?而有的不能?
回答一下一個比較specific的問題:為什麼C不容易反編譯。
首先,明確定義一下問題。反編譯的input是可執行程序,在這個帖子里我會叫它binary。output是C代碼。binary是電腦執行的一行行指令。所以我們會想,先把binary還原成指令。這個過程叫做disassembling,還原出的指令叫做disassembly。
Disassembly是很難全對的。幾乎不可能。這個看似簡單的動作其實很複雜。一般是兩種方法,動態和靜態。動態方法是,讓計算機跑程序,跑了什麼記錄下來。但是你怎麼知道計算機把程序跑全了呢?靜態方法是,不讓計算機跑,而是讓計算機讀這些指令。binary會有entry point,然後你就知道從binary的哪裡開始讀啦,然後就一行行讀唄。但是,仍然會有你讀不到的地方。entry point一般是main()的第一行,然後main讀完了你讀哪裡?有些function可能main根本沒調用。別說反彙編,即使把一個binary裡面的函數都找全找對都很難。在此賣一下自己的paper: BYTEWEIGHT: Learning to Recognize Functions in Binary Code. 裡面有一個section介紹了萬惡的optimization會把function變成什麼樣的噁心的代碼。
然後,即使Disassemble完了,之後要做的是還原各種變數。同樣很難,目前為止幾乎不可能。有兩方面的工作要做。一個叫做variable recovery, 一個叫做type recovery。一般做這兩部之前會把disassembly再轉化成另一種中間語言。這樣做的目的是把一些有side effect的指令中那些隱藏信息都explify出來,比如,你在做加法的時候有可能會影響那些flag寄存器,通過中間語言,「這些寄存器可能也會被改變」這樣的信息就會明確表達出來。有可能一條disassembly對應幾條中間語言的語句。
還原完變數和變數的type,就要開始還原程序的真正結構了。還原真正結構還是很難完全正確。比如這樣的一個control flow graph:
x &<-
| |v |x &<-----| | |v | |x __| || |
v |x_____||v...這種結構只能用goto語句來表達。evaluate反編譯的其中一個指標是goto指令。反編譯越少使用goto指令,說明結構更明晰,也就被認為反編譯的效果更好。但是這個指標是有問題的,稍後再說。
類似於上面的很難用for/while循環和if-then-else表達的結構在現實的binary中很多。嗯,最後再說反編譯的evaluation問題。首先,goto語句的多少並不能完全客觀地衡量反編譯的效果如何。有可能源代碼自帶goto呀,有可能源代碼其實寫的時候很挫啊,你反編譯完還優化了本身的代碼能。誰能定奪?其次,很難有自動的辦法來評價本來的代碼和反編譯完的代碼是equivalent的。如果原來的代碼是while,反編譯完的代碼用for,是不能說反編譯完是錯的。所以反編譯的evaluation也一直是一個值得探討推敲的話題。
就醬。寫得比較散比較跳,大家見諒。要理解這個問題,先要看「正」編譯的過程是怎樣的。
你有一個想法,這是一種人類自然語言可以表達出來的東西。你利用編程技能,把它「翻譯」成你熟悉的一種編程語言:這個過程叫做編程。然後你使用編譯器(compiler)將它翻譯成機器所能理解的語言:這個過程叫做編譯。編程和編譯都是「信息丟失」的過程。比如你說,我有一組整數,我要把這些數排個序,然後輕車熟路地寫了個冒泡排序。然而一定程度上,你的原始動機其實已經從代碼里丟失了——有經驗的人可以一眼看出這段代碼是在排序,而新手小明看到的只有一些 for 和 if 之類的東西。如果是更複雜的功能,可能過一段時間你自己都看不懂自己當時是想幹什麼。從程序語言到機器語言的過程其實也是一樣的。這兩個過程其實都是把「做什麼」轉換成「怎麼做」的過程,轉換完成之後,究竟一開始是要做什麼,這個信息已經丟失了。
所謂「反編譯」,其實就是找回這些丟失的信息的過程。從這個角度上來說,你閱讀一段代碼的過程,其實就是在將它「反編譯」成自然語言。如果要完美地反編譯,那隻存在一種可能,就是信息完全沒有丟失——比如說你閱讀的這段代碼有充分的注釋,或者它使用了一種你所知曉的模式(這也是為什麼大家一再強調注釋和設計模式的重要性)。對於從機器語言到程序語言的反編譯過程,也是一樣。
比如說有比反編譯更低級(非貶義)的過程,叫做反彙編:
嚴格來說彙編語言也是一種編程語言,不過我們在這裡把它和我們常說的高級編程語言(包括C語言)區分開。
這個步驟里,我在彙編和機器語言里使用的是雙向實線箭頭,因為它們是可以互相轉換的。從彙編語言到機器語言的過程中沒有丟失任何信息——因為兩者的指令是一一對應的,因此反彙編可以輕鬆達成。
這就是很多程序語言只能反彙編、不能(難以,下同)反編譯的原因。一般我們管這種語言叫「編譯語言」,又稱「原生語言」。代表有C、C++等。
那為什麼有的語言可以反編譯呢?這又要從機器語言說起。就像不同地域的人所用的語言不同一樣,不同的機器說的語言也不盡相同。用行話說,叫「指令集不同」。比方說,你的電腦和你的手機,指令集通常是不一樣的。一段程序要讓不同的機器都能執行,只能分別翻譯(編譯)成相應的機器語言。這個過程太麻煩了,於是人們想了個辦法,搞出了一種叫解釋語言的東西(此處未考證解釋語言是否就是因此發明的,只是幫助理解)。如下圖:
解釋語言有兩種執行方式,這取決於執行端的「解釋器」是如何工作的。一種是直接解釋執行,中間就沒有機器語言什麼事情了,但這種方式效率很低。因此現代的解釋語言基本上都會採用第二種方式,也就是經由圖中上部的路徑,先通過JIT編譯的方式翻譯成機器語言,然後再執行,保證執行效率。JIT編譯大致可以理解為「用到什麼就編譯什麼」,這個過程常常是在執行過程中同步進行的。
「解釋器」的英文interpreter,其實就是名詞「翻譯」的意思。這好比你國外交部發了封文件到各國大使館,再由大使館的工作人員分別翻譯成相應的語言,傳達給目標國相關部門。代表性的解釋語言如Javascript,它要在不同機器的瀏覽器上都能正確執行,所以採用這種方式。但是這樣一來,程序代碼就必須提供給每一台執行端機器了。這可是泄密啊。對於防止泄密,最直接的方式自然是加密。有鎖就有鑰匙,同時也有開鎖術;有加密解密,也有相應的破解方式。這時候所謂的「反編譯」,其實就是破解加密演算法。這一點就不展開聊了。後來,人們覺得解釋語言執行得實在有點慢,於是又想了一個辦法:把一些可以前期做掉的工作先做掉,只留著那些跟目標機器有關的工作,到時候再說。於是程序被處理成了一種叫做「中間語言」,或者叫「位元組碼」的東西:
這個過程一般也叫做編譯。中間語言辭彙少,比較精鍊,執行起來也更快。這些語言一般也會用上JIT技術,進一步把中間語言編譯成機器語言(而非解釋執行),執行效率也就跟那些原生的編譯語言不相上下了。這種語言代表性的有C#、Java等。程序語言可以編譯成中間語言,反過來,中間語言也可以在一定程度上反編譯成程序語言。這是因為採用這種編譯方式的編程語言為了保證它們的高級特性(比如說反射),在編譯的過程中保留了源程序的絕大部分信息,只有很少的信息丟失;也正是因為丟失了這一部分信息,中間語言通常不能完美地反編譯——最常見的就是反編譯出來的程序中局部變數的名字都丟了,被替換成了由反編譯器自動生成的名字。但這樣反編譯出來的程序,結構和功能都是完備的,可讀性也有一定的保障。一般來說,我們所說的可以反編譯的程序都是指這樣一類語言寫就的程序。
中間語言可以被反編譯;加密又會被破解,而且執行前還要解密,會帶來額外的性能開銷。有沒有辦法能讓代碼既能有效執行,又不被截獲代碼的人所利用呢?這時候人們從一些職業素養很差的程序員那裡得到了啟發。
實現一個相同的功能,可以有無數種形式的代碼。你恪盡職守,認認真真地寫注釋,準確地命名函數和變數, 嚴格按照規範進行縮進和換行;小明卻相反,完全沒有注釋,變數全部用abcd乃至故意誤導別人(var mySon = laowang.Son),縮進換行邋遢,尤其是在大括弧前不換行,讓大家很不滿。於是老闆想,我們先把小明開除掉,然後給你發獎金並要求大家按照你的方式寫代碼,並且開發一個工具,喚作「混淆器」,在發布時再把代碼處理成小明寫的那種樣子:
這樣代碼即使被反編譯和解密了,別人看也看不懂,不小心還會被帶到坑裡去。代碼畢竟是寫給人看的,只是偶爾讓機器跑一跑,所以沒有可讀性的代碼是沒有價值的。這種方法一出,廣受好評,於是變成了一種非常普遍的做法。注意圖中省略了中間代碼和JIT的步驟,混淆通常會跟這些技術一起使用。並不是不能被反編譯,只是還沒發明一個通用的反編譯演算法。你把一個東西交給人,人花一些時間,總可以給你還原出代碼的
因為寫編譯器的人把人能看懂的東西翻譯成機器語言就已經非常不容易了,而且過程中間丟棄了非常多的信息,沒人想過要翻譯回來。
能反彙編的一般都是跑虛擬機的,有中間語言,所以簡單。以C語言為例,主要還是因為做編譯器的人壓根就沒想過要把彙編變回源代碼。否則,假如設計之初就考慮了做編譯器的時候同時做一個反編譯器,最後做出一個C語言的編譯器和反編譯器,這個編譯器編譯出來的彙編能夠較好的還原成源代碼,是完全可能的。
包含在ELF文件里的符號就有類似這樣的功能,能幫助人們更好的調試理解已經編譯成二進位的程序。
這麼說吧
程序語言,是給人設計的,叫做高級語言機器跑的東西,可以叫做低級語言編譯就是高級到低級,從你能看懂,到你看不懂(適合機器執行)反編譯,就是從低級到高級舉個例子,
ceylon -&> .java -&> .class -&> 機器碼編譯,一般是從人寫的語言,到機器語言,這中間的過程中,
不但會丟失很多信息(代碼結構),有些優化會導致代碼語義的改變所以總體來說,在這個鏈上往回走的越遠,越難恢復
這是原因12、鏈上的每一步,抽象級別跨度越大,越難反編譯
像jvm的二進位碼反編譯就很容易,但是到了機器碼,由於和人能看懂的語言抽象過於不同,所以非常難反編譯3、即使有現成的工具和演算法,反編譯出來的和原來長的大概率是不一樣的c還算好的,在編譯語言裡面,算是原代碼與彙編對應關係比較明確的了。也就是變數的名字擦除了,不過慢慢猜就可以了。你去看看c++,就屬於背後的黑魔法比較多的。
然後再來看看jvm code。你會發現,它的每個局部變數和參數都有個專用的區域。每個變數的操作都有個load-操作-store的過程。這樣每條原代碼對應的中間代碼就清楚很多。當然c語言也可以那樣寫,但是性能會下降,語句數量會多一些,就失去本來的優勢了。謝邀,面向一切輸入的通用反彙編問題就是停機問題,是做不到的
簡單說,食物變翔很容易,反之則困難,,當然也有例外,比如金針菇~~
題主可以試下我寫的這個工具:DLL to C下載地址:Dll Decompiler
應該先問是不是再問為什麼?
混小本論文時發現一句話,其實都是可以反編譯的,但反編譯出來的東西和源頭差距有多大就沒人知道了。因為它不是個一對一的字典翻譯。
比如:彙編里一個的跳轉表,死啃幾個小時發現是個switch case。下次再看另一個卻是虛函數表。
再比如:跟蹤一個煩人的函數半天不知它在做什麼,偶然看到它訪問的數據內容,才知道它是某開源庫里的md5實現。所以,翻譯(反編譯)到哪種地步才算是到位?哪種程度算「能反編譯」,哪種算「不能」?都能被反彙編呀,因為不管用什麼技術防破解,一個程序始終是要載入到內存被CPU執行的。然後通過分析彙編代碼能(非100%)還原出開發語言的代碼。
樓主的意思是解釋型語言被混淆之後沒法弄回來吧?正常編譯軟體流程是
一個現代編譯器的主要工作流程如下: 源代碼 (source code) → 預處理器 (preprocessor) →編譯器 (compiler) → 彙編程序 (assembler)(這兩個過程疑似wikipedia寫錯了) → 目標代碼 (object code) → 鏈接器 (Linker) → 執行檔 (executables)
from 編譯器
你拿到C++的2進位文件,可以變成彙編文件,但是解釋型的語言直到運行的時候才翻譯成機器碼的,比如python給終端用戶的其實是混淆後的代碼(簡單的打個包)或者根本就是原來的源代碼,本來就沒被編譯過,就無所謂反編譯了。
有些函數存在單值反函數,而有些函數卻不存在。
推薦閱讀: