#2020徵文-開發板#SYS_RUN()和MODULE_INIT()之間的那些事
接觸鴻蒙裝置開發有一段時間了,也是時候好好挖一挖鴻蒙裝置程式的啟動流程了。
破冰問題:鴻蒙裝置程式從哪裡開始執行的?
相信大家都已經非常清楚了,鴻蒙裝置程式需要指定入口函式,具體表現在程式碼層面就是通過語句 SYS_RUN(app_entry); 指定,其中 app_entry 是裝置程式入口函式名;而整個鴻蒙裝置的啟動流程也可以順理成章的挖掘出來。如下圖:
這看起來非常完美了,解決了所有問題!可是,我覺得還是有不清楚的地方,即:MODULE_INIT(run) 幹了什麼事?為什麼最終會呼叫到 app_entry() 這個入口函式?
接下來,我們逐個問題的解決!
本質問題:MODULE_INIT(run) 幹了什麼事?
要弄清楚這個問題,就得先來講講 SYS_RUN() 究竟是什麼?!有同學可能會認為 SYS_RUN(app_entry); 是一個函式呼叫語句,將裝置程式入口地址註冊到系統中,進而呼叫。從原理上這麼理解沒錯,可細節上根本不是那麼回事! SYS_RUN() 在用法上很像函式,但本質是一個巨集!必須強調: 在 C 語言中無法在函式之外進行函式呼叫,而 SYS_RUN(app_entry); 出現的位置並不在任何函式中,所以它不可能是函式呼叫。那會是什麼呢?真相只有一個,只可能是一個定義(宣告)語句。為了證明這個結論,我們將 SYS_RUN() 這個巨集徹底扒光了看個透徹。如下:
最終,我們可以知道:SYS_RUN(app_entry); 是定義了一個名為 __zinitcall_run_app_entry 的函式指標,其型別是 InitCall,無論是否使用都不會編譯報錯,並且強制編譯使其最終存放在名為 .zinitcall.run2.init 的段中。
好!接下來就可以直接分析 MODULE_INIT(run) 了。
MODULE_INIT(run) 展開之後根本看不出和 app_entry 有任何關係啊!我們用了九牛二虎之力把巨集掰開了,可結果貌似一無所獲!!!MODULE_INIT() 和 SYS_RUN() 之間的關係還是非常不明朗,接下來該怎麼辦呢?
仔細觀察這兩個巨集拼接出來的符號!
可以發現它們都和 zinitcall 有關,並且我們也知道了 SYS_RUN(app_entry) 定義的全域性指標就放在名為 .zinitcall.run2.init 的段中,所以可以推測:這個兩個巨集的關係是通過連結指令碼關聯的。
接下來,通過工具檢視目標檔案的段資訊和符號資訊。
通過輸出可以知道,在名為 .zinitcall.run2.init 的段中確實存在 __zinitcall_run_app_entry 這個符號。
之後,動手翻原始碼。。。。
經過努力,我們可以找到 \code-1.0\vendor\hisi\hi3861\hi3861\build\build_tmp\scripts\link.lds 檔案,並且發現如下的指令碼程式碼:
這樣就真相大白了:
SYS_RUN(app_entry) 定義的函式指標 __zinitcall_run_app_entry 通過強制編譯的方式進入 .zinitcall.run2.init 段中。在連結指令碼中定義的兩個符號 __zinitcall_run_start (理解為陣列名)和 __zinitcall_run_end 分別指向 __zinitcall_run_app_entry 所在資料段的起始位置和結束位置。 又因為 MODULE_INIT(run) 的功能就是遍歷 __zinitcall_run_start 和 __zinitcall_run_end 所指定的區域(理解為函式指標陣列),並呼叫每個單元(指標)所指向的函式,因此,__zinitcall_run_app_entry 所指向的函式必然被呼叫,即:app_entry() 必然被呼叫。
更進一步閱讀這個連結指令碼可知:目標檔案中的 .zinitcall.run2.init 段最終會被連結並彙編進一個名為 .zInit 的資料段中!
檢視最終可執行程式中的符號資訊和段資訊可證明這個結論。
最終可執行程式中存在 __zinitcall_run_app_entry , __zinitcall_run_start 以及 __zinitcall_run_end , 並且 __zinitcall_run_app_entry 和 __zinitcall_run_start 的地址均為 0x004aeb1c,根據輸出的段資訊可知,它們均位於 .zInit 段中。證畢!
總結:
通過強制編譯連結構成一個全域性指標陣列(每個 SYS_RUN() 定義一個數組元素)
在連結指令碼中定義符號自動確認這個陣列的起始地址和結束地址
MODULE_INIT() 通過遍歷的方式呼叫陣列元素所指向的函式
PS:
大家如果感興趣可以自己親手動手實驗一下,所需材料和工具可在文末附件中下載。
1)編譯附件中的 hello_world 工程(基於Hi3861)
2)將下面編譯得到的目標檔案拷貝到工具目錄
\code-1.0\out\wifiiot\obj\applications\sample\wifi-iot\app\hello_world\hello_world.o
\code-1.0\out\wifiiot\Hi3861_wifiiot_app.out
3)執行命令觀察結果
./nm hello_world.o
./objdump -h hello_world.o
./nm Hi3861_wifiiot_app.out
./objdump -h Hi3861_wifiiot_app.out
感嘆一下,這真是一個精妙絕倫的設計方案!這樣設計,理論上支援任意多的裝置程式,開發者只需要簡單的指定程式入口即可,完全不用關心背後的機制,也不用擔心最多支援多少程式的問題。
這一招,學到了!!!
文中涉及多個進階知識點,可參考狄泰軟體學院《C語言進階剖析教程》和《唐老師的私房課》。
作者:唐佐林
想了解更多內容,請訪問: 51CTO和華為官方戰略合作共建的鴻蒙技術社群https://harmonyos.51cto.com