非常規(guī)入門之一:通用編程語言技術(shù)之上下文隱患(三)

在上文中,引入函數(shù)與接口的概念以將機器特性以及相關(guān)底層細節(jié)進行隱藏,其優(yōu)勢在于使用戶(程序員)不必時刻關(guān)心底層的硬件邏輯,從而在一定程度上提高開發(fā)人員的開發(fā)效率。但是機器特性與相關(guān)底層細節(jié)的隱藏并不意味著開發(fā)人員能夠在抽象層面為所欲為,這反而要求必須關(guān)心機器配置和相關(guān)的代碼的效率問題。
故而在接下來的文章中,會為大家?guī)碓诔橄髮用孢M行功能開發(fā)需要注意的一些細節(jié)。同時,這一部分也是通用編程語言技術(shù)向上提供強大功能的基礎(chǔ)。請勿將此“基礎(chǔ)”與前兩章相提并論。前兩章內(nèi)容是通用編程語言技術(shù)得以實現(xiàn)的基礎(chǔ)。此處所指“向上”乃是面向應用、邏輯或抽象層。

一、由函數(shù)帶來的上下文問題
在前文中提到,函數(shù)的調(diào)用過程依次為如下9個步驟。在本節(jié)中,我們的重點放在發(fā)生跳轉(zhuǎn)的部分,即第1、2、3、8、9步。
1、獲取上下文,即函數(shù)名
2、跳轉(zhuǎn)至函數(shù)所在地址
3、保護上下文環(huán)境,即將之前的調(diào)用位置保存在棧中
4、為局部數(shù)據(jù)開辟空間
5、執(zhí)行函數(shù)體
6、將返回值保存在RAX寄存器中(可能有、可能沒有)
7、釋放局部數(shù)據(jù)的空間(開辟多少就釋放多少)
8、預備恢復上下文環(huán)境,將保存在棧中的前一個調(diào)用位置取出。
9、恢復上下文環(huán)境,即跳轉(zhuǎn)至前一個調(diào)用位置
在之前的文章中,我們提到標識符是有效數(shù)據(jù)存儲在內(nèi)存中物理地址的字符化表示,同時我們也將函數(shù)視為一種特殊的數(shù)據(jù)。忘記的小伙伴可以通過第一章“基礎(chǔ)”的第五節(jié)中進行回顧。
函數(shù)名作為標識符進行識別,其在底層中也就是一個地址,只是在這個地址的內(nèi)存空間中存儲的是一段有意義的、能夠執(zhí)行的邏輯代碼。同時,函數(shù)又可以稱為“子程序”、“過程”,因而在匯編語言中,函數(shù)的章節(jié)中,一般會使用“子程序設計”或“過程調(diào)用”等類似的說法,我們只需知道函數(shù)就是子程序、就是過程。
當然,可能有小伙伴會將函數(shù)和計算機組成原理中的中斷進行類比,雖然二者有一定的區(qū)別,但這種區(qū)別微乎其微,以至于基本沒有什么比較好的例子來舉證,所以也就可以把中斷當成函數(shù)來理解。
在上文列舉的步驟中,我們可以得知,函數(shù)正式調(diào)用之前,先獲取了函數(shù)所在內(nèi)存中的地址(即函數(shù)名)。此處會有若干個陷阱,那就是:1、這個函數(shù)所在的地址不存在;2、這個函數(shù)所在的地址上是空操作
下面進行分析:
1、函數(shù)所在地址不存在
在通用編程語言技術(shù)中,使用的語法都是高級語言語法,所以在編譯時中就會將這種由用戶(程序員)行為缺失導致的致命的語法錯誤識別出來。
如果編譯功能比較弱,不進行語法錯誤識別(當然,這是不太可能的),就需要由底層語言負責錯誤識別;如果底層語言仍然無法識別,則有程序在運行時觸發(fā)程序錯誤中斷。
2、函數(shù)所在地址為空操作
若空操作由高級語言語法轉(zhuǎn)換而來,則必然不會出現(xiàn)純粹的空操作,在函數(shù)末尾會出現(xiàn)ret指令返回調(diào)用處;若純空操作(匯編語言nop指令)是由用戶(程序員)利用某些機制內(nèi)嵌匯編語言形成的(必然不可能),則由底層語言進行優(yōu)化;若底層語言無法進行優(yōu)化(必然不會出現(xiàn)),則有程序在運行時觸發(fā)程序運行時中斷(由于程序運行無操作觸發(fā),應稱之為“警告”,因為其并未造成程序的崩潰)。
故而可以得出以下結(jié)論:
1、函數(shù)的調(diào)用行為包含三個動作:獲取函數(shù)地址,跳轉(zhuǎn),保存跳轉(zhuǎn)前地址。
2、在第1點中,涉及到的兩個地址稱為上下文環(huán)境。
3、函數(shù)調(diào)用的三個動作分別稱為調(diào)用前,調(diào)用時,調(diào)用后;“調(diào)用前”時刻的上下文就已經(jīng)確定下一個上下文;“調(diào)用時”時刻,上下文發(fā)生變化;“調(diào)用后”時刻,上下文將一直保持到函數(shù)返回后。
當函數(shù)的內(nèi)部邏輯處理完成后,應當還原到上一個上下文,但是,如果在函數(shù)調(diào)用后不進行保存,則整個程序的邏輯依舊會發(fā)生異常。所以,自高級語言誕生以來,函數(shù)調(diào)用的細節(jié)就始終交由編譯器進行處理,上下文環(huán)境也由編譯器增加的語句保存至棧中。故而伴隨函數(shù)調(diào)用的行為就是環(huán)境保護。注意:保存的是前一個上下文,而不是調(diào)用后的當前上下文,因為當前上下文就在寄存器中,無需保存。
全局狀態(tài)下理論上不需要環(huán)境保護,但如今計算機系統(tǒng)中,運行時(即進程和線程)不只一個,所以全局環(huán)境也會由編譯器增加若干語句,以完成全局環(huán)境的保護。
函數(shù)完成后的返回行為又可稱為上下文環(huán)境的恢復。包含兩個動作:獲取保存的上下文、返回??赡苓@里“返回”二字會使人滿臉問號。事實上,在匯編語言中函數(shù)執(zhí)行后的返回行為只包含pop和ret兩個指令。pop用于將保存的上下文重新寫入RBP寄存器,RET指令用于跳轉(zhuǎn)至前一個上下文環(huán)境。
二、上下文環(huán)境的延伸:命名空間
命名空間是更高級、更復雜、動態(tài)的上下文環(huán)境,在命名空間中能夠存儲更為復雜的數(shù)據(jù)。
在前一節(jié)中,我們使用上下文環(huán)境可以得知函數(shù)之間的調(diào)用關(guān)系,很明顯,這只能用于面向過程。如果我們需要向面向?qū)ο筮^渡,就需要知道當前函數(shù)所屬于哪一個對象。
在上下文環(huán)境中,我們可以直接訪問RBP寄存器和棧就可以得知上下文環(huán)境,那么命名空間呢?
很顯然,我們無法直接獲取命名空間,故而需要將其專門保存下來。實際上,如果你沒有學過C++或者沒有處理類似的問題,你可能永遠也碰不到命名空間的概念。
命名空間是用來組織和重用代碼的重要手段。這是一句廢話。然而就是這句廢話,卻能夠讓“通用編程語言技術(shù)”實現(xiàn)從面向過程到面向?qū)ο蟮馁|(zhì)性飛躍。
我們知道,一個類的不同對象的屬性值可能不一樣,但是對象使用的方法的內(nèi)部邏輯都是一致的,那么這些方法存儲在哪里呢?當然是全局區(qū)域。因為類的方法,不屬于任何一個對象。但是類的方法可能不止一個,如果允許方法同名,則參數(shù)有可能不一樣。如此該怎樣確定是哪一個對象在使用哪一個方法呢?
首先,我們應該確定三條基本原則:
1、命名空間的名稱應當符合標識符的正規(guī)式(或正則表達式,都是一個東西)
2、命名空間的名稱在所在作用域中具有唯一性。
3、命名空間允許嵌套一個或多個命名空間。
根據(jù)以上三條原則,我們可以立即找到一個命名空間,即某一個程序的全局命名空間(namespace Global),當然進程之間是不會重名的,程序的運行由操作系統(tǒng)控制,程序一旦成功申請內(nèi)存空間,操作系統(tǒng)就會為它分配一個有效的唯一的進程號,該進程號既可以用于操作系統(tǒng)管理進程的銷毀、內(nèi)存釋放,同時還可以作為這個進程內(nèi)部最頂層的命名空間。進程與進程之間是相對獨立的。進程間通信則需要依賴操作系統(tǒng)提供的服務。
既然命名空間名稱是一個標識符,也就意味著命名空間實際在機器內(nèi)部并不存在,即命名空間也是一個有效的地址。
而至于命名空間在物理內(nèi)存中存儲格式,實際是需要根據(jù)底層設計進行實現(xiàn)的,這也就意味著不同平臺可能實現(xiàn)的方式不一樣,不同編程語言的實現(xiàn)方式也不盡相同。
至此,不知道有沒有小伙伴發(fā)現(xiàn)一個問題:類和命名空間的作用如此類似!既然如此類似,那么類是不是特殊的命名空間呢?作者個人認為,類應該算是一種特殊的命名空間,但是類和命名空間仍然存在區(qū)別。
命名空間的作用看起來比類的作用要少很多。命名空間的作用就是用來組織和重用代碼,但是類的功能遠不止于此。其實也沒有必要在命名空間和類之間糾結(jié)。
三、上下文的夢魘:上下文對象
隨著更多的編程語言支持Lambda表達式(點名C++,其他高級語言早就支持了,C++11才支持),上下文對象的身影越來越多,即使只是用JavaScript,上下文對象也能迷惑一陣子。
Lambda表達式,一些文檔成也稱之為匿名函數(shù)(如此說是因為如此稱呼并不一定符合編程語言特性,比如Java中就不完全正確)。
Lambda表達式又稱為閉包,其形式大致符合“參數(shù)列表=>內(nèi)部邏輯”。但是由于不同語言的函數(shù)定義方式不同,這也導致不同語言的閉包格式也是千奇百怪。
雖然以上代碼看似簡單,但是“萬惡”的JavaScript又提供了一個讓人歡喜讓人狂的功能,如下:
1、JavaScript中閉包可以賦值給變量,此時變量稱為函數(shù)對象
2、JavaScript能夠通過函數(shù)形式構(gòu)造對象
3、函數(shù)自調(diào)用,形如
4、箭頭函數(shù)不是函數(shù)
前3條都好理解,唯有最后一條。作者的理解是JavaScript引擎將箭頭函數(shù)直接解析為表達式,函數(shù)體內(nèi)部的上下文對象仍與外部一致。與一般的閉包形成對比,JavaScript中閉包的函數(shù)體內(nèi)部的上下文對象與外部不一致,即JavaScript引擎將閉包解析成Window對象的一個方法,當條件觸發(fā)時,由Window對象來調(diào)用這個函數(shù),這也是前3條存在的因素。
或許大家能夠猜測到,本節(jié)所言上下文對象,實際指的就是this這個對象。想要理解this的作用,只需要理解其底層的基礎(chǔ),也就是前2節(jié)的內(nèi)容。

至此,面向?qū)ο蟮牡讓舆壿嬀徒Y(jié)束了。通用編程語言技術(shù)也由面向過程編程過渡到了面向?qū)ο缶幊獭?/p>