筆者來聊聊分散加載的東西。
1、什么是分散加載
程序是靜態的概念,有數據有代碼,都是存在不同的區域,但是進程是動態的概念,主進程在運行的時候,會實際修改對應的數據,還有在上電加載的時候將數據段搬到對應的位置,都是屬于運行態,由程序執行來保證。
分散加載會把Code與Data放在指定的區域,保證程序在進入main函數后正常運行,如果有多個Code或者Data的時候,會分別加載到對應的區域,不會直接按照起始地址連著一起加載。
比如上圖,在可執行的視圖里面,分散加載會找到對于的Code、Data地址,然后加載,對于一些其他段,比如bss段會進行初始化為0的操作。
如果全部按照Code和data這種順序加載,那在執行視圖里面則會出現順序錯誤,比如Code3加載到bss1,導致程序執行異常。
2、分散加載的作用
2.1 ARMCC 編譯器分散加載代碼
本文以STM32的啟動為介紹,在介紹分散加載啟動之前,介紹一下STM32的啟動方式,總共有三種啟動方式。
根據選定的啟動模式,主閃存存儲器、系統存儲器或SRAM可以按照以下方式訪問:
從主閃存存儲器啟動:主閃存存儲器被映射到啟動空間(0x0000 0000),但仍然能夠在它原有的地址(0x0800 0000)訪問它,即閃存存儲器的內容可以在兩個地址區域訪問, 0x00000000或0x0800 0000。
從系統存儲器啟動:系統存儲器被映射到啟動空間(0x0000 0000),但仍然能夠在它原有的地址(互聯型產品原有地址為0x1FFF B000,其它產品原有地址為0x1FFF F000)訪問它。
從內置SRAM啟動:只能在0x2000 0000開始的地址區訪問SRAM。當從內置SRAM啟動,在應用程序的初始化代碼中,必須使用NVIC的異常表和偏移寄存器,從新映射向量表到SRAM。
本文中介紹的是:從主閃存啟動,也就是內置的主閃存Flash啟動。
上圖為 一個簡單的STM32 加載與執行視圖的繪制,鏈接腳本指定Code 從0x0800 0000開始,RW ZI 從0x2000 0000開始放置。
LR_IROM10x080000000x00010000{;loadregionsize_region ER_IROM10x080000000x00010000{;loadaddress=executionaddress *.o(RESET,+First) *(InRoot$$Sections) .ANY(+RO) } RW_IRAM10x200000000x00020000{;RWdata .ANY(+RW+ZI) } } 12345678910
左邊加載視圖即靜態的Code和Data放置方式,比如download的時候 兩者把axf 解析成bin文件,然后燒錄到nor flash中,可以看到其實靜態放置的位置 關系不是很大,主要是執行的時候 位置正確就行 ,因為Code中有絕對地址,不然PC跑飛。
執行視圖即程序正常運行的時候 Code或者Data放置的位置。
燒錄的位置 和 程序執行的位置不同,分散加載 負責講其加載到對應位置,保證main 函數執行正常
圖中BSS段為初始化為0 或者未初始化的全局變量,不占用ImageSIze(bin文件大小),所以加載視圖中并沒有其,執行視圖必須有,上電的時候會將這部分初始化為0。
綜述函數的作用
來看看具體的分散加載代碼,是如何搬運data 和初始化bss段的。(下文中中斷向量表偏移0x10000 偏移64K)
armcc 手冊里面介紹:__main 和 __rt_entry 是初始化運行態的環境,以及后面運行APP程序。
通俗點來講__main函數初始化運行態的環境,主要的功能就是做分散加載將Code位置搬運正確,才能正常運行Code。其作用如下:
將section 拷貝到對應的執行域地址執行,(把RO RW從加載域拷貝到執行域,如果有壓縮的Section 會進行解壓縮并進行拷貝)
還有bss 段的初始化,將其初始化為0,
之后跳到__rt_entry。
以及堆棧的初始化,
lib庫的初始化
跳到對應的用戶程序(main)。
main函數結束后,調用exit函數。
手冊內容如下:
__user_setup_stackheap
初始化堆棧地址,以及SP指針位置
__scatterload_copy
主要是RW data的拷貝
__scatterload_zeroinit
主要是ZI data的初始化
__rt_entry如下圖armcc 手冊所說:
建立堆棧
初始化C庫(方便固件使用C庫)
調用main函數
關閉C庫
離開
啟動代碼的簡單介紹
0x08010188F000F802__main:bl0x8010190;__scatterload_rt2 1 0x0801018CF000F83Cbl0x8010208;__rt_entry 1
跳到初始化堆棧區域,執行完成之后,跳到main函數。
10x08010190A00A__scatterload_rt2:adrr0,0x80101BC 20x08010192E8900C00ldmr0,{r10,r11} 30x080101964482addr10,r10,r0 40x080101984483addr11,r11,r0 50x0801019AF1AA0701sub.wr7,r10,#0x1 12345
第一句adr指令,其作用就是將地址讀到寄存器中,接著,以r0為基地址,讀取r0+3464地址的值,將其放到r10,r0+3464+4 的值 ,將其放到r11,然后,r10+=r0,r11+=r0,r7=r10-1
而 0x08013620 ~ 0x08013640 是一個region表,記錄著加載域或者執行域的地址信息,從map文件中也可以看到一些信息,
根據鏈接腳本信息,RW的起始地址0x2000 0000,前三個信息:RW 起始地址,數量size,拷貝的函數后三個信息:ZI 起始地址,數量size,初始化為0的函數
備注學習:ldm 指令,與stm指令是一對,加載指定地址的數據LDM{cond} mode Rn{!}, reglist{^} 讀取Rn 地址中的數據,放到寄存器,并且之后地址自增,再次讀取STM{cond} mode Rn{!}, reglist{^} 以Rn 地址為基地址,將寄存器的值放到基地址內存,并且之后地址自增,再次寫入
10x0801019E45DA__scatterload_null:cmpr10,r11 20x080101A0D101bne0x80101A6 30x080101A2F000F831bl0x8010208;__rt_entry 40x080101A6F2AF0E09adrr14,0x80101A1 50x080101AAE8BA000Fldmr10!,{r0-r3} 60x080101AEF0130F01tstr3,#0x1 70x080101B2BF18itne 80x080101B41AFBsubner3,r7,r3 90x080101B6F0430301orrr3,r3,#0x1 100x080101BA4718bxr3 110x080101BC00003464dcd0x3464 120x080101C000003484dcd0x3484 123456789101112
第一行:比較r10 r11 r10 是region表的首地址,先讀三個,后讀三個數據,之后就等于r11,直接跳到__rt_entry第二行:如果不等,跳轉到第三行,第三行:如果相等,則跳到__rt_entry第四行:則將地址到r14 ,用于返回。第五行:讀取RW的地址信息,size 和拷貝函數地址,r0-r3 可以看到region的信息被讀出來了。
第六-第十行:將跳轉地址轉成奇數地址,用于bx指令跳轉,080101C4 -> 080101C5,之后跳到拷貝函數。
10x080101C43A10__scatterload_copy:subsr2,r2,#0x10 20x080101C6BF24ittcs 30x080101C8C878ldmcsr0!,{r3-r6} 40x080101CAC178stmcsr1!,{r3-r6} 50x080101CCD8FAbhi0x80101C4;__scatterload_copy 60x080101CE0752lslsr2,r2,#0x1D 70x080101D0BF24ittcs 80x080101D2C830ldmcsr0!,{r4,r5} 90x080101D4C130stmcsr1!,{r4,r5} 100x080101D6BF44ittmi 110x080101D86804ldrmir4,[r0] 120x080101DA600Cstrmir4,[r1] 130x080101DC4770bxr14 140x080101DE0000movsr0,r0 1234567891011121314
r2 是RW data的size 信息,r0 是 RW的起始地址信息,0x08013640,從map信息也可以看到
r0:0801340r1:20000000地址第一到第五行:是一個循環語句,每次拷貝16個字節,到RAW區域,即0x2000 0000地址中。對于78個字數據 拷貝70個后,最后八個數字沒辦法拷貝,不滿足CS第六行:左移29位,將個數清零。r2 = 0-8 = 0xFFFF FFF8第七行 第八行:拷貝最后八個數據到RAW數據第十 十一 十二行:不滿MI (復數)則直接跳過 如果滿足的話,則拷貝最后一個數據,最后跳轉到 cmp r10 r11 ,進行bss 段的初始化。
備注:BHI則表示大于則跳轉,看之前文章介紹,ARM學習(2) 寄存器的理解 ===》通用寄存器及狀態寄存器IT:if then 分支指令,后面如果滿足狀態標志位,則執行,否則直接跳過,lsl:左移指令,MI:為負數則執行
10x080101E02300__scatterload_zeroinit:movsr3,#0x0 20x080101E22400movsr4,#0x0 30x080101E42500movsr5,#0x0 40x080101E62600movsr6,#0x0 50x080101E83A10subsr2,r2,#0x10 60x080101EABF28itcs 70x080101ECC178stmcsr1!,{r3-r6} 80x080101EED8FBbhi0x80101E8 90x080101F00752lslsr2,r2,#0x1D 100x080101F2BF28itcs 110x080101F4C130stmcsr1!,{r4,r5} 120x080101F6BF48itmi 130x080101F8600Bstrmir3,[r1] 140x080101FA4770bxr14 1234567891011121314
獲取到bss段的數據后,可以看到r0-r3更新信息,數據size 為 0x0728個
第一到第八行:同樣則是循環語句,每次初始化16個字的數據為0,第九行:同樣左移29位,將數據清零第十行 到 第 十一行:將最后八個字節數據寫入0第十二行 到第十三行:同上面一致(數據拷貝)
數據初始化完成后,同樣跳轉到 cmp r10 r11,則相等,跳到 __rt_entry
0x080101FCB51F__rt_lib_init:push{r0-r4,r14} 0x080101FEF003FA09__rt_lib_init_fp_1:bl0x8013614;_fp_init 0x08010202BD1F__rt_lib_init_alloca_1:pop{r0-r4,pc} 0x08010204B510__rt_lib_shutdown:push{r4,r14} 0x08010206BD10__rt_lib_shutdown_cpp_1:pop{r4,pc} 0x08010208F003F9BE__rt_entry:bl0x8013588;__user_setup_stackheap 0x0801020C4611movr1,r2 0x0801020EF7FFFFF5__rt_entry_li:bl0x80101FC;__rt_lib_init 0x08010212F000F811__rt_entry_main:bl0x8010238;main 0x08010216F003F9F0bl0x80135FA;exit 0x0801021AB403__rt_exit:push{r0,r1} 0x0801021CF7FFFFF2__rt_exit_ls:bl0x8010204;__rt_lib_shutdown 0x08010220BC03__rt_exit_exit:pop{r0,r1} 0x08010222F000FAF5bl0x8010810;_sys_exit 0x080102260000movsr0,r0 12345678910111213141516171819
__rt_enry:進入到初始化堆棧的地方:初始化堆棧的位置,以及SP指針。
之后初始化 fp_init,需要用到p10 協處理器,之后再研究。__rt_entry_main:進入main函數。則分散加載完成。
0801360C4800__user_libspace:ldrr0,0x8013610;r0,=__libspace_start 0801360E4770bxr14 0801361020000140dcd0x20000140;__libspace_start 123
libspace_start:lib庫 空間使用 棧空間,在初始化堆棧空間的時候。
1080135884675__user_setup_stackheap:movr5,r14 20801358AF000F83Fbl0x801360C;__user_libspace 30801358E46AEmovr14,r5 4080135900005movsr5,r0 5080135924669movr1,r13 6080135944653movr3,r10 708013596F0200007bicr0,r0,#0x7 80801359A4685movr13,r0 90801359CB018addsp,sp,#0x60 100801359EB520push{r5,r14} 11080135A0F7FDFA3Cbl0x8010A1C;__user_initial_stackheap 12080135A4E8BD4020pop{r5,r14} 13080135A8F04F0600mov.wr6,#0x0 14080135ACF04F0700mov.wr7,#0x0 15080135B0F04F0800mov.wr8,#0x0 16080135B4F04F0B00mov.wr11,#0x0 17080135B8F0210107bicr1,r1,#0x7 18080135BC46ACmovr12,r5 19080135BEE8AC09C0stmr12!,{r6-r8,r11} 20080135C2E8AC09C0stmr12!,{r6-r8,r11} 21080135C6E8AC09C0stmr12!,{r6-r8,r11} 22080135CAE8AC09C0stmr12!,{r6-r8,r11} 23080135CE468Dmovr13,r1 24080135D04770bxr14 123456789101112131415161718192021222324
第八行 到 第十二行:r13 = 20000140 加 0x60之后,正好指向 lib庫的地址的棧頂。2000 01A0.第十八行:r12 = 2000 0140,后面初始化 lib 庫 后 變成 r12 = 2000 0180第二十三行:r13 指向棧頂 2000 07A0
可以看到向量表的第一個地址也是 2000 07A0,符合預期。
__user_initial_stackheap LDRR0,=Heap_Mem 08010A1C4804__user_initial_stackheap:ldrr0,0x8010A30 LDRR1,=(Stack_Mem+Stack_Size) 08010A1E4905ldrr1,0x8010A34 LDRR2,=(Heap_Mem+Heap_Size) 08010A204A05ldrr2,0x8010A38 LDRR3,=Stack_Mem 08010A224B06ldrr3,0x8010A3C BXLR 08010A244770bxr14 08010A260000dcw0x0 08010A2808010371dcd0x8010371;SystemInit 08010A2C08010189dcd0x8010189;__main 08010A30200001A0dcd0x200001A0;Heap_Mem 08010A34200007A0dcd0x200007A0;__initial_sp 08010A38200003A0dcd0x200003A0;Stack_Mem 08010A3C200003A0dcd0x200003A0;Stack_Mem 123456789101112131415161718
初始化堆棧的地址 開始位置,將其存儲到 r0- r3中。
可以看到R0、R1和R2 分別存放的是堆的基地址、棧的基地址(棧頂)和堆的極限地址(與arm cc手冊說的一致。)
來看看armcc 手冊上面是怎么介紹堆棧初始化函數的:
__user_initial_stackheap 函數將棧基地址(棧頂)值放在r1中,可以從上面寄存器看到,確實寄存器r1存放的是棧頂的地址。
__user_setup_stackheap 初始化sp的地址為棧頂地址(上面的代碼中第二十三行)
__user_initial_stackheap 之所以可以使用C語言來編寫,是因為 __user_setup_stackheap 提供了一個 臨時的棧,(手冊可能比較老,和代碼有點出入。)
審核編輯:湯梓紅
-
程序
+關注
關注
117文章
3792瀏覽量
81163 -
Code
+關注
關注
0文章
69瀏覽量
15397 -
編譯器
+關注
關注
1文章
1636瀏覽量
49173 -
分散加載
+關注
關注
0文章
3瀏覽量
1450
原文標題:聊聊程序分散加載啟動的奧秘
文章出處:【微信號:技術讓夢想更偉大,微信公眾號:技術讓夢想更偉大】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論