越來越多的工作現如今都交給了編譯器,甚至連動態代碼修改的數據組織這種事都交給了編譯器。gcc提供了一個特性用于嵌入式匯編,那就是asm goto,其實這個特性沒有什么神秘之處,就是在嵌入式匯編中go to到c代碼的label,其最簡單的用法如下(來自gcc的文檔):
asm goto其實就是在outputs,inputs,registers-modified之外提供了嵌入式匯編的第四個“:”,后面可以跟一系列的c語言的label,然后你可以在嵌入式匯編中go to到這些label中一個。然而使用asm goto可以巧妙地將“一個大家都能想到的點子”規范化,就是說你只需要調用一個統一的接口--一個宏,編譯器就將你想實現的東西給實現了,要不然代碼寫起來會很麻煩,這點上,編譯器不嫌麻煩。這一個大家都能想出的點子的由來還得從內核的效率說起。
以下的代碼來自lwn的《Jump label》:
即使有了unlikey優化,既然有if判斷,cpu的分支預測就有可能失敗,再者do_trace在代碼上離if這么近,即使編譯器再聰明,二進制代碼的do_trace也不會離前面的代碼太遠的,這樣由于局部性原理和cpu的預取機制,do_trace的代碼很有可能就被預取入了cpu的cache,就算我們從來不打算trace代碼也是如此。
我們需要的是如果不開啟trace,那么do_trace永遠不被欲取或者被預測,唯一的辦法就是去掉if判斷,永遠不調用goto語句,像下面這樣:
在運行時修改載入內存的二進制代碼就是我們大家都能想到的點子,就是說在運行的時候當我們知道trace_foo_enabled在某一時刻被設置為0的時候,我們動態的將二進制代碼修改掉,將if代碼段去掉,這樣一個分支預測就不存在了,而且trace_foo_enabled這一個變量也不需要再被訪問了(該變量在內存中,訪問它肯定會涉及load/flush cache的動作,為了一個很可能沒有用的變量操作cache很不值)。提前要說的是,我們可以使用這種方式去掉所有的分支預測,然而這并不可取,因為程序是動態運行的,很多用于判斷的變量值都是根據程序的執行瞬息萬變,正是這種根據判斷結果采取不同動作的機制給與了程序靈活性,如果每當我們確定一個值時就修改二進制代碼取消分支預測的話,其本身的開銷將會遠遠大于分支預測的開銷,更重要的是,緊接著那個值又變化了,我們不得不再次修改二進制代碼,這期間要訪問那個變量好幾次。所以,只有在我們確定不經常變化的變量的判斷上才能用這種方式取消分支預測,而像trace與否的判斷正好符合我們的需求。
gcc編譯器提供了asm goto的機制來滿足我們的需求,使得我們可以在asm goto的基礎上構建出一個叫做jump label的東西。下面的代碼段說明了jump label的用法和原理:
標號0僅僅執行一個nop,不涉及cache,后面的pushsection保存現有的section,很多情況下當前的section就是text,然后定義一個“表”,表中有兩個元素:0b和trace#NUM,其實就是兩個標號,在asm goto機制中,標號還可以更多,它們在嵌入式匯編的最后一個“:”后面依次排布。這些標號就是供選擇的標號,執行流將跳入其中的一個標號處,具體跳到哪一個就看當前的二進制代碼被修改成了“跳到哪一個”,因此asm goto為我們做的僅僅是提供一個地方(一個“:”)供我們將label傳入,保存了一系列的表還是需要我們的c代碼邏輯--jump label實現,這些表(其實就是一系列的三元組)方便我們根據這些表來修改運行中的二進制代碼,最終修改二進制代碼還是要由我們自己寫代碼完成的。
有了這個asm goto以及我們jump label代碼的支持,內核對于是否trace這種小事就再也不用愁了(使用中的kernel一般是不用trace的,只有在出了問題以后或者調試內核時才使用trace,因此在主代碼中加入“是否trace”的判斷實在是一種沉重的負擔),如果對于某一個函數不需要trace,內核只需要執行一個操作將asm goto附近的代碼改掉即可,比如改稱下面這樣:
如果需要trace,那么就改成:
這一切在kernel中的用法如下:
第一行的“1”是一個標號,該標號后的代碼執行的內容就是nop-第二行,第三行重新開始了一個section,這樣的意義很大,下面的三元組:[instruction address] [jump target] [tracepoint key]的二進制代碼就不會緊接著標號1(nop)了,這個三元組就是jump label機制的核心,指示了所有可能跳轉到的標號,這里的技巧在于標號1,標號1也作為一個合法的可能跳轉到的標號存在,和標號label是并列的,由于pushsection和popsection的存在,上面的代碼匯編結果看起來是下面這樣:
如果啟用了trace,那么只需要將標號1修改成標號label就可以了:
內核之所以能夠找到需要修改代碼的地址,就是借助于上面說的那個三元組(instruction address,jump target,tracepoint key),其中instruction address就是這個地址,在linux的JUMP LABEL機制中,它固定為標號1,也就是nop的標號,如果不啟用trace,那么直接執行nop,如果啟用了trace,那么將nop修改為jmp label即可,如果后來又禁用了trace,只需將它再次修改成三元組中的標號1即可,這一切過程中,三元組本身是不會改變的。注意,三元組中的tracepoint key在jump label機制中并沒有什么實質的意義,它僅僅是為了組織kernel中“是否trace”變量用的,所有的“是否trace”變量組織成一個鏈表,鏈表的每一個節點下面掛著另一個子鏈表,該子鏈表中元素是所有使用這個“是否trace”變量的代碼環境,包括代碼的地址,標號的地址等。
下面看一下kernel對于JUMP_LABEL的實現框架。首先看一下三元組的數據結構:
其次一個比較重要的數據結構是一個key節點,表示一個“是否trace”的變量:
啟用一個trace意味著需要將一個key(類似于trace_foo_enabled)設置為1,然后修改所有判斷該key的代碼附近的二進制代碼:
以上就是使用asm goto實現的jump label,在2.6.37內核中被引入。
附:.section以及.previous
在匯編語言中使用.section和.previous指令可以將它們之間的代碼編譯到不同的section中,也就是不緊接著.section上面的代碼。linux kernel中的異常處理就是用這兩個偽指令實現的,定義了一個叫做fix的section和一個叫做ex_table的section,可能出現exception的代碼用一個標號表示,ex_table中保存了一些二元組(出現異常代碼的標號,異常處理程序的標號),異常處理程序在fix這個section中,這樣雖然代碼看起來是下面這樣:
然而編譯器會將fix和ex_table放到離text很遠的地方的,這樣cpu預取時就不會將fix或者ex_table的代碼預取到執行cache了,只有在發生異常的時候才會使用fix和ex_table,而發生異常畢竟是一種罕見現象,這就是一種優化。
原文標題:asm goto與JUMP_LABEL
文章出處:【微信公眾號:Linuxer】歡迎添加關注!文章轉載請注明出處。
責任編輯:haq
-
代碼
+關注
關注
30文章
4887瀏覽量
70260 -
編譯器
+關注
關注
1文章
1655瀏覽量
49891
原文標題:asm goto與JUMP_LABEL
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
Python在嵌入式系統中的應用場景
嵌入式系統中的代碼優化與壓縮技術
如何提高嵌入式代碼質量?
新手怎么學嵌入式?
HAL庫在嵌入式系統中的應用
嵌入式學習建議
什么是嵌入式?一文讀懂嵌入式主板
嵌入式主板是什么意思?嵌入式主板全面解析
一種常用嵌入式開發代碼庫

評論