??在分析之前首先查閱 RT-Thread 的官方文檔 [RT-Thread 自動初始化機制](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic?id=rt-thread-自動初始化機制),根據官方文檔的講述在 RTT 源碼中一共使用了 6 中順序的初始化,本文以其中的一個 INIT_APP_EXPORT(fn) 為例進行自動初始化的原理分析,其他順序的初始化的原理與之一致。
初始化順序 | 宏接口 | 描述 |
1 | INIT_BOARD_EXPORT(fn) | 非常早期的初始化,此時調度器還未啟動 |
2 | INIT_PREV_EXPORT(fn) | 主要是用于純軟件的初始化、沒有太多依賴的函數 |
3 | INIT_DEVICE_EXPORT(fn) | 外設驅動初始化相關,比如網卡設備 |
4 | INIT_COMPONENT_EXPORT(fn) | 組件初始化,比如文件系統或者 LWIP |
5 | INIT_ENV_EXPORT(fn) | 系統環境初始化,比如掛載文件系統 |
6 | INIT_APP_EXPORT(fn) | 應用初始化,比如 GUI 應用 |
1 知識點補充
1.1 __attribute__ 關鍵字
??1. 關鍵字__attribute__ 是 GNU C 實現的編譯屬性設置機制,也就是通過給函數或者變量聲明屬性值,以便讓[編譯器](https://so.csdn.net/so/search?q=編譯器&spm=1001.2101.3001.7020)能夠對要編譯的程序進行優化處理。
??2. 關鍵字 __attribute__((section(x))) 是告訴編譯器,將作用的函數或數據放入指定名為 ”x” 輸入段中。 舉個例子,看下面一段代碼:
int a __attribute__(section(“var”)) = 0;
??定義了一個整形變量 a,然后將其賦值為0,而中間的 __attribute__(section(“var”)) 語句的作用是將變量 a 放入指定的段 var 中。而如果不指定變量所處的段的話,編譯器就會隨機將其分配在內存中。
??3. __attribute__((used)) 的含義是即使它們沒有被引用,也留在目標文件中,也就是告訴編譯器,我聲明的這個符號是需要保留的。
1.2 函數指針
1.2.1 簡單的函數指針的運用
??使用簡單的函數指針的示例如下
#include
/* 定義了一個指針變量p,該變量指向某種函數,這種函數有兩個int類型參數,返回一個int類型的值*/
/* 只有第一句我們還無法使用這個指針,因為我們還未對它進行賦值 */
int (*p)(int, int);
/* 定義了一個求和函數 */
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int sum = 0;
p = add; // 將函數 add() 的地址賦值給變量 p
printf("add = %08X\n", *((unsigned int *)add));
printf("*add = %08X\n", *((unsigned int *)*add));
printf("&add = %08X\n", *((unsigned int *)&add));
printf("p = %08X\n", *((unsigned int *)p));
printf("*p = %08X\n", *((unsigned int *)*p));
printf("&p = %08X\n", *((unsigned int *)&p));
sum = (*p)(1, 2);
//sum = p(1, 2); // 這兩種寫法都可以
printf("sum = %d\n", sum);
return 0;
}
/* 運行結果 */
add = 0xE5894855
*add = 0xE5894855
&add = 0xE5894855
p = 0xE5894855
*p = 0xE5894855
&p = 0x0040052D // 變量p的地址與函數指針值
sum = 3
??從上面的結果看來,我們應該把函數名,當成指針看待。最常見的函數調用方式:fnc1(); 只是 (*fnc1)(); 簡寫形式而已。我們之所以可以 fnc1(); 這樣調用函數,只是編譯器幫我們做了調整。對于函數名 fnc1 來說,不管是 *fnc1 還是 fnc1 還是 &fnc1,編譯器都認為他是函數指針。
1.2.2 使用 typedef 定義的函數指針
??使用 typedef 定義的函數指針的示例如下
#include
/* typedef 的功能是定義新的類型,就是定義了一種 pFunc 的類型 */
/* pFunc 這種類型為指向某種函數的指針,這種函數以兩個int類型為參數并返回int類型 */
typedef int (*pFunc)(int, int);
/* 定義了一個求和函數 */
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int sum = 0;
pFunc p = add; // 使用 pFunc 這種類型定義了變量p,將函數 add() 的地址賦值給變量 p
printf("add = 0x%08X\n", *((unsigned int *)add));
printf("*add = 0x%08X\n", *((unsigned int *)*add));
printf("&add = 0x%08X\n", *((unsigned int *)&add));
printf("p = 0x%08X\n", *((unsigned int *)p));
printf("*p = 0x%08X\n", *((unsigned int *)*p));
printf("&p = 0x%08X\n", *((unsigned int *)&p));
sum = (*p)(1, 2);
//sum = p(1, 2); // 這兩種寫法都可以
printf("sum = %d\n", sum);
return 0;
}
/* 運行結果 */
add = 0xE5894855
*add = 0xE5894855
&add = 0xE5894855
p = 0xE5894855
*p = 0xE5894855
&p = 0x0040052D
sum = 3
1.3 鏈接腳本解析
??摘抄 RT-Thread 鏈接腳本中和本文有關的內容如下所示
/* section information for initial. */
. = ALIGN(4); /* 按照四字節對齊 */
__rt_init_start = .; /* 開始一個 "片段" */
KEEP(*(SORT(.rti_fn*))) /* 告訴鏈接器保留 ".rti_fn*" 的段,并將其排序 */
__rt_init_end = .; /* 結束一個 "片段" */
??其中 SORT 關鍵字的含義是鏈接器會在把文件和 section 放到 輸出文件中之前按名字順序重新排列它們。
??該鏈接腳本部分定義了申明各種自動初始化函數在進行鏈接時的排列順序,因為 RT-Thread 源碼中一共定義了六種實現自動初始化功能的宏接口,詳見本文最開始的表格,所以也就可以解釋為什么 INIT_BOARD_EXPORT(fn) 在自動初始化的最開始,而 INIT_APP_EXPORT(fn) 在自動初始化的結尾,就是因為 SORT 關鍵字在起作用。
2 自動初始化原理分析
??RT-Thread 源碼中一共有六種不同順序的自動初始化宏定義,如下所示
/* rt-thread/include/rtdef.h */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
2.1 自動初始化宏定義解析
??查看 RT-Thread 的源碼中自動初始化宏定義,以 INIT_APP_EXPORT(fn) 為例進行分析, INIT_APP_EXPORT(fn) 的定義如下。其中宏定義中 ## 連接符號由兩個井號組成,其功能是在帶參數的宏定義中將兩個子串(token)聯接起來。
/* rt-thread/include/rtdef.h */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
#define INIT_EXPORT(fn, level) RT_USED const init_fn_t __rt_init_##fn RT_SECTION(".rti_fn." level) = fn
??其中里面的宏定義如下
#define RT_USED __attribute__((used))
typedef int (*init_fn_t)(void); /* init_fn_t 為函數指針 */
#define RT_SECTION(x) __attribute__((section(x)))
??以文件 rt-thread/components/finsh/shell.c 中 Finsh 控制臺初始化函數 INIT_APP_EXPORT(finsh_system_init)為例,參照上面的宏定義規則分布展開和最終結果如下
INIT_APP_EXPORT(finsh_system_init)
|-> INIT_EXPORT(finsh_system_init, "6")
|-> RT_USED const init_fn_t __rt_init_finsh_system_init RT_SECTION(".rti_fn." "6") = finsh_system_init
|-> __attribute__((used)) const init_fn_t __rt_init_finsh_system_init __attribute__((section(".rti_fn.6"))) = finsh_system_init
??結合第1小節的補充知識,上述宏定義展開的最終結果的含義為:定一個一個 init_fn_t 類型的函數指針變量 __rt_init_finsh_system_init ,將 finsh_system_init() 函數的地址賦值給了定義的變量, 并且將該變量放到了指定的段 ".rti_fn.6" 中。
??所以說只要在代碼中使用 INIT_APP_EXPORT(fn) 申明的初始化函數最終都會定義在指定的段 ".rti_fn.6" 中。
2.2 組件初始化調用解析
??參考官方文檔 [RT-Thread 啟動流程](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic?id=rt-thread-啟動流程),在調度器的啟動函數執行時,會調用 rt_components_init() 函數對申明的各種初始化函數進行調用,RT-Thread 的啟動流程如下圖所示。

??函數 rt_components_init() 的源碼如下所示,因為沒有定義宏 RT_DEBUG_INIT,所以直接將和宏 RT_DEBUG_INIT 有關的代碼省略掉
/* rt-thread/src/components.c */
void rt_components_init(void)
{
#if RT_DEBUG_INIT
... ... /* 省略掉與分析無關的代碼 */
#else
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
#endif /* RT_DEBUG_INIT */
}
??其中 __rt_init_rti_board_end 和 __rt_init_rti_end 表示不同的段。在系統源碼中又定義了幾個空函數來申明了一些段,如下所示。
/* rt-thread/src/components.c */
/* 宏定義展開后段名為 ".rti_fn.0" ,函數指針變量為 __rt_init_rti_start */
static int rti_start(void)
{
return 0;
}
INIT_EXPORT(rti_start, "0");
/* 宏定義展開后段名為 ".rti_fn.0.end" ,函數指針變量為 __rt_init_rti_board_start */
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
/* 宏定義展開后段名為 ".rti_fn.1.end" ,函數指針變量為 __rt_init_rti_board_end */
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
/* 宏定義展開后段名為 ".rti_fn.6.end" ,函數指針變量為 __rt_init_rti_end */
static int rti_end(void)
{
return 0;
}
INIT_EXPORT(rti_end, "6.end");
??上面幾個函數的導出,再加上6個自動初始化宏定義的導出,結合 1.3 小節連接腳本的分析,可以得到各個段名 和 對應的函數指針 / 宏的名字 及 前后順序如下表所示。
section 名 | 函數指針 / 宏 |
.rti_fn.0 | __rt_init_rti_start |
.rti_fn.0.end | __rt_init_rti_board_start |
.rti_fn.1 | INIT_BOARD_EXPORT(fn) |
.rti_fn.1.end | __rt_init_rti_board_end |
.rti_fn.2 | INIT_PREV_EXPORT(fn) |
.rti_fn.3 | INIT_DEVICE_EXPORT(fn) |
.rti_fn.4 | INIT_COMPONENT_EXPORT(fn) |
.rti_fn.5 | INIT_ENV_EXPORT(fn) |
.rti_fn.6 .rti_fn.6.end |
INIT_APP_EXPORT(fn) __rt_init_rti_end |
??我們可以通過編譯生成的 map 文件對上述的分析進行驗證,map 文件通常位于工程的 Debug 目錄下,在 map 文件中搜索 .rti_fn* 可以找到如下所示的內容。可以看到經過排序后的各個自動初始化的段和對應的函數指針,我們前面分析的 Finsh 自動初始化的函數指針 __rt_init_finsh_system_init 就位于段 ".rti_fn.6" 中。并且從里面我們還可以看出每個函數指針都占 4 個字節的空間,因為在 32 位的系統中無論什么樣的指針都占 4 個字節的空間。
*(SORT(.rti_fn*))
.rti_fn.0 0x08075220 0x4 ./rt-thread/src/components.o
0x08075220 __rt_init_rti_start
.rti_fn.0.end 0x08075224 0x4 ./rt-thread/src/components.o
0x08075224 __rt_init_rti_board_start
.rti_fn.1 0x08075228 0x4 ./rt-thread/components/utilities/ulog/ulog.o
0x08075228 __rt_init_ulog_init
.rti_fn.1 0x0807522c 0x4 ./drivers/drv_clk.o
0x0807522c __rt_init_clock_information
.rti_fn.1 0x08075230 0x4 ./drivers/drv_spi.o
0x08075230 __rt_init_rt_hw_spi_init
.rti_fn.1 0x08075234 0x4 ./drivers/drv_wdt.o
0x08075234 __rt_init_rt_wdt_init
.rti_fn.1 0x08075238 0x4 ./applications/peripheral/src/hn_psram.o
0x08075238 __rt_init_rt_hw_psram_init
.rti_fn.1.end 0x0807523c 0x4 ./rt-thread/src/components.o
0x0807523c __rt_init_rti_board_end
.rti_fn.2 0x08075240 0x4 ./rt-thread/components/utilities/ulog/backend/console_be.o
0x08075240 __rt_init_ulog_console_backend_init
.rti_fn.2 0x08075244 0x4 ./rt-thread/components/utilities/ulog/ulog.o
0x08075244 __rt_init_ulog_async_init
.rti_fn.2 0x08075248 0x4 ./rt-thread/components/net/lwip-2.0.3/src/arch/sys_arch.o
0x08075248 __rt_init_lwip_system_init
.rti_fn.2 0x0807524c 0x4 ./rt-thread/components/drivers/src/workqueue.o
0x0807524c __rt_init_rt_work_sys_workqueue_init
.rti_fn.2 0x08075250 0x4 ./rt-thread/components/dfs/src/dfs.o
0x08075250 __rt_init_dfs_init
.rti_fn.3 0x08075254 0x4 ./drivers/drv_rtc.o
0x08075254 __rt_init_rt_hw_rtc_init
.rti_fn.4 0x08075258 0x4 ./rt-thread/components/net/sal_socket/src/sal_socket.o
0x08075258 __rt_init_sal_init
.rti_fn.4 0x0807525c 0x4 ./rt-thread/components/libc/pthreads/pthread.o
0x0807525c __rt_init_pthread_system_init
.rti_fn.4 0x08075260 0x4 ./rt-thread/components/libc/compilers/gcc/newlib/libc.o
0x08075260 __rt_init_libc_system_init
.rti_fn.4 0x08075264 0x4 ./rt-thread/components/libc/compilers/common/time.o
0x08075264 __rt_init__rt_clock_time_system_init
.rti_fn.4 0x08075268 0x4 ./rt-thread/components/dfs/filesystems/elmfat/dfs_elm.o
0x08075268 __rt_init_elm_init
.rti_fn.4 0x0807526c 0x4 ./packages/ppp_device-v1.1.0/class/ppp_device_ec20.o
0x0807526c __rt_init_ppp_ec20_register
.rti_fn.4 0x08075270 0x4 ./applications/peripheral/src/hn_spi_flash.o
0x08075270 __rt_init_hn_spi_flash_init
.rti_fn.6 0x08075274 0x4 ./rt-thread/components/finsh/shell.o
0x08075274 __rt_init_finsh_system_init
.rti_fn.6 0x08075278 0x4 ./packages/ppp_device-v1.1.0/samples/ppp_sample.o
0x08075278 __rt_init_ppp_sample_start
.rti_fn.6 0x0807527c 0x4 ./applications/user/src/hn_pvd_detect.o
0x0807527c __rt_init_pvd_detect_init
.rti_fn.6 0x08075280 0x4 ./applications/user/src/hn_smtp.o
0x08075280 __rt_init_hn_smtp_init
.rti_fn.6 0x08075284 0x8 ./applications/peripheral/src/hn_spi_flash.o
0x08075284 __rt_init_hn_spi_flash_filesystem_init
0x08075288 __rt_init_hn_easyflash_init
.rti_fn.6.end 0x0807528c 0x4 ./rt-thread/src/components.o
0x0807528c __rt_init_rti_end
0x08075290 __rt_init_end = .
0x08075290 . = ALIGN (0x4)
[!provide] PROVIDE (__ctors_start__, .)
??再結合上面的 rt_components_init() 函數中的具體實現源碼分析,我們可以看到函數指針在 for 循環的開始指向了 __rt_init_rti_board_end,也就是指向了函數 rti_board_end(),該函數沒有任何操作直接返回了0。
??當滿足條件fn_ptr < &__rt_init_rti_end 時,fn_ptr每次循環自加1,根據生成的 map 文件可以看出下一次就指向了 __rt_init_ulog_console_backend_init ,也就是指向了函數 ulog_console_backend_init() ,該函數對 ulog輸出到控制臺進行了初始化。
??每次循環過程中fn_ptr自加1,然后執行對應的初始化函數,一直到 fn_ptr 自加后等于 &__rt_init_rti_end時循環結束,在這個過程中就執行了各種自動初始化的代碼,完成了自動初始化的任務。
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
??為什么fn_ptr = &__rt_init_rti_board_end 這個地方要使用取地址 & 符號呢?因為 fn_ptr是一個指向函數指針的指針,根據本文 2.1 小節的分析 __rt_init_rti_board_end 是一個函數指針變量,也就是說 fn_ptr 最開始指向的是函數指針變量 __rt_init_rti_board_end 的地址,根據 map 文件也就是 0x0807523c,指針每次循環自加1也就是加上4個字節的大小,就指向了下一個位置 0x08075240,(*fn_ptr)(); 也就是把該位置的內容取出來也就是相當于 __rt_init_ulog_console_backend_init();。
3 總結
??為什么 RT-Thread 要采用這種復雜的方式來進行自動初始化操作呢?我認為是 RT-Thread 采用和 Linux 一樣的機制,為了實現驅動和應用的分層將驅動的初始化操作在 main() 函數之前就進行初始化后注冊好,等到運行到 main() 函數后,用戶只需要關心應用層的額代碼即可,如果不采用這種方式而是采用裸機寫法的方式在main() 函數的最開始進行初始化,就需要執行很多驅動層的初始化代碼,就不能很好的實現驅動和應用的分層,也就不能很好的實現應用層只需關系應用層的邏輯而不用關系驅動和硬件初始化的分層思想了。
審核編輯:湯梓紅
-
初始化
+關注
關注
0文章
50瀏覽量
12021 -
RT-Thread
+關注
關注
31文章
1337瀏覽量
41301 -
函數指針
+關注
關注
2文章
57瀏覽量
3894
發布評論請先 登錄
相關推薦
價值89元的嵌入式RT-Thread設計書籍僅需5積分免費帶回家!(手慢無!限20人)
為什么RT-Thread要采用這種復雜的方式來進行自動初始化操作呢
【原創精選】RT-Thread征文精選技術文章合集
RT-Thread自動初始化原理分析
RT-Thread啟動過程分析RT-Thread自動初始化機制分析

RT-Thread學習筆記 --(3)RT-Thread自動初始化機制分析

評論