1. 程式人生 > 實用技巧 >#2020徵文-開發板#SYS_RUN()和MODULE_INIT()之間的那些事

#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