1.概述
編譯器將編寫的C程序代碼進(jìn)行翻譯,變成機(jī)器可以執(zhí)行的程序,這個大致上可以分為四個步驟:預(yù)編譯、編譯、匯編、鏈接。
其中編譯和鏈接這兩個過程比較重要。編譯過程就是將源代碼通過程序翻譯后生成機(jī)器可以認(rèn)識的機(jī)器語言。而鏈接就是將目標(biāo)文件進(jìn)行組合,最后生成在特定平臺上可以正常運(yùn)行的可執(zhí)行程序。
本文主要描述鏈接這個過程。由于匯編器生成的目標(biāo)代碼(.o)文件不能被立即執(zhí)行,因?yàn)槔锩嬉话愣紩渌脑次募械姆?、變量或者函?shù)調(diào)用等等,要想處理好這些問題,就必須將程序進(jìn)行鏈接。
2.靜態(tài)鏈接和動態(tài)鏈接
根據(jù)開發(fā)人員指定庫函數(shù)的鏈接方式,鏈接又分為動態(tài)鏈接和靜態(tài)鏈接兩種。
2.1 靜態(tài)鏈接
我們在進(jìn)行嵌入式開發(fā)過程中時(shí),往往接觸到比較多的就是靜態(tài)鏈接。前面說過,編譯器將源代碼編譯成一個一個的.o文件的目標(biāo)文件,這些文件又會存在各種依賴關(guān)系,所以將各種.o文件匯集到一起。
這種方式編譯出來的程序,可以直接運(yùn)行,不依賴于外部庫文件。
2.2 動態(tài)鏈接
當(dāng)涉及到程序比較多的時(shí)候,如果每個程序都依賴于同樣的一個庫里面的函數(shù),那么這個庫就是共享的。
2.3 兩種鏈接方式的對比
靜態(tài)鏈接方式,適合單應(yīng)用程序,比如嵌入式rtos等等。這種將所有的目標(biāo)文件都鏈接到一個可執(zhí)行的文件中,所以執(zhí)行效率很高。但是文件內(nèi)存占用大。動態(tài)鏈接時(shí),如果app1運(yùn)行將libc加載到內(nèi)存中,下次app2直接可以從內(nèi)存中使用。這種方式可以讓每個程序的文件大小比較小,但是相對于的,執(zhí)行效率相對比較低。
3.鏈接腳本
一般在進(jìn)行g(shù)cc進(jìn)行鏈接的時(shí)候,都會考慮到鏈接腳本(linker script),該文件一般以lds文件作為后綴名。該文件規(guī)定了將特定的section放到文件內(nèi),并且控制著輸出文件的布局。一般來說,自己編寫的鏈接腳本可以指定傳遞參數(shù)-T xxx.lds,其中xxx.lds則是自己編寫的鏈接腳本。
xxx.lds基本格式如下:
SECTIONS{sections-commandsections-command......}
那么什么是sections?每個目標(biāo)文件都有一些列的段,比如代碼段、數(shù)據(jù)段、bss段等等。
3.1 鏈接腳本實(shí)例分析
如果沒有實(shí)際的東西,那么說起理論來將索然無味。下面就具體來看下面的一個鏈接腳本的布局。
一個最簡單的linker腳本文件如下:
SECTIONS{.=0x10000;/*(1)*/.text:{*(.text)}/*(2)*/.=0x800000;/*(3)*/.data:{*(.data)}/*(4)*/.bss:{*(.bss)}/*(5)*/}
下面來解釋一下上述的程序
(1).的定義是location counter,也就是把當(dāng)前的程序指向0x10000,如果沒有這個地址,默認(rèn)該符號的值為0?;蛘咴趃cc的鏈接選項(xiàng)中-Ttext 0x10000也是一樣的效果。
(2).text指向代碼段,其中*這個符號代表所有的輸入文件的.text section合并成的一個
(3).=0x800000將定位器的符號設(shè)置成0x800000
(4).data指向所有輸入文件的數(shù)據(jù)段,并且這個地址的起始為0x800000
(5).bss表示所有輸入文件的bss段
上述從一個最簡單的鏈接腳本分析了鏈接腳本的語法格式。
3.2 內(nèi)存的分段鏈接
如果一塊內(nèi)存在sram中,一塊內(nèi)存在sdram中,這兩塊地址并不連續(xù),那么需求是將代碼段(.text)段放在sram區(qū),數(shù)據(jù)段(.data)與bss段放在ddr區(qū),這時(shí)鏈接腳本該如何進(jìn)行設(shè)計(jì)。
首先假設(shè)sram的空間地址為0x1000處開始的,可用空間為1M。ddr的地址空間為0x40000000,目前只用到2M。
首先可用在lds文件中做一個聲明
MEMORY{ram : org = 0x00001000, len = 1Mddr : org = 0x40000000, len = 2M}
然后鏈接腳本可用以如下的方式進(jìn)行編寫
SECTIONS{ . = 0x00001000; . = ALIGN(4096); .text:{*(.text)}>ram.data:{*(.data)}>ddr.bss:{*(.bss)}>ddr}
只需要指定對應(yīng)的鏈接段即可。
3.3 指定第一個文件的鏈接
有的時(shí)候,需要考慮到鏈接順序的問題,比如在有些處理器中,系統(tǒng)從一個固定的地址啟動,但這個地址一定最開始的時(shí)候會存放一個異常向量表。從異常向量表中跳轉(zhuǎn)到實(shí)際的入口函數(shù)處去執(zhí)行。那么這該如何進(jìn)行設(shè)計(jì)?
一般來說我們鏈接代碼段的時(shí)候,都是鏈接的.text section。但是,我們也可用指定該文件的代碼段。比如可以在第一個需要編譯的文件頭部加上
.section ".text.entrypoint"
這樣就會指定
SECTIONS{ . = 0x00001000; . = ALIGN(4096); .text: { KEEP(*(.text.entrypoint)) *(.text) }>ram.data:{*(.data)}>ddr.bss:{*(.bss)}>ddr}
其中keep相當(dāng)于告訴編譯器,這部分不要被垃圾回收。
3.4 自己定義代碼段名字
有些時(shí)候,需要將特定的符號指定到特定的地址,這樣的好處就是可用通過地址訪問對應(yīng)的函數(shù)。這個應(yīng)用在rt-thread rtos操作系統(tǒng)應(yīng)用的比較經(jīng)典。
在很多時(shí)候,需要指定初始化的執(zhí)行順序。比如驅(qū)動的初始化順序等等。實(shí)現(xiàn)這種功能有很多種實(shí)現(xiàn)方式,上中下策都可以,下策就是直接通過函數(shù)調(diào)用關(guān)系進(jìn)行調(diào)用。中策就是采用回調(diào)函數(shù)的方式進(jìn)行設(shè)計(jì)。上策就是利用linker script進(jìn)行函數(shù)擴(kuò)展。
直接調(diào)用的方式實(shí)現(xiàn)起來比較簡單,也比較好理解,直接調(diào)用對應(yīng)的函數(shù)即可。
回調(diào)函數(shù)就是利用函數(shù)指針,當(dāng)回調(diào)函數(shù)綁定了指針時(shí),執(zhí)行該回調(diào)函數(shù)檢查該函數(shù)是否綁定,然后選擇執(zhí)行。這樣可用降低耦合性。
采用linker script方式時(shí),相當(dāng)于把函數(shù)的指針集合到一個.text的空間中。這樣執(zhí)行的時(shí)候,只需要找到linker中對應(yīng)的地址,轉(zhuǎn)換成函數(shù)即可,這種方式就很好擴(kuò)展。
在rt-thread中,函數(shù)導(dǎo)出命令使用了這種技巧
/* board init routines will be called in board_init() function */#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1") /* pre/device/component/env/app init routines will be called in init_thread *//* components pre-initialization (pure software initilization) */#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")/* device initialization */#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")/* components initialization (dfs, lwip, ...) */#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")/* environment initialization (mount disk, ...) */#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")/* appliation initialization (rtgui application etc ...) */#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
而INIT_EXPORT的實(shí)現(xiàn)如下:
#define INIT_EXPORT(fn, level) RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn." level) = fn
而在鏈接腳本中編寫如下:
. = ALIGN(4);__rt_init_start = .;KEEP(*(SORT(.rti_fn*)))__rt_init_end = .;. = ALIGN(4);
最后可用查看map文件,查看地址
*(SORT(.rti_fn*)) .rti_fn.0 0xffffffff802bd418 0x8 buildkernelsrccomponents.o 0xffffffff802bd418 __rt_init_rti_start .rti_fn.0.end 0xffffffff802bd420 0x8 buildkernelsrccomponents.o 0xffffffff802bd420 __rt_init_rti_board_start .rti_fn.1 0xffffffff802bd428 0x8 builddriversdrv_gpio.o 0xffffffff802bd428 __rt_init_loongson_pin_init .rti_fn.1.end 0xffffffff802bd430 0x8 buildkernelsrccomponents.o 0xffffffff802bd430 __rt_init_rti_board_end .rti_fn.2 0xffffffff802bd438 0x8 buildkernelcomponentsdfssrcdfs.o 0xffffffff802bd438 __rt_init_dfs_init .rti_fn.2 0xffffffff802bd440 0x8 buildkernelcomponents etlwip-2.0.2srcarchsys_arch.o 0xffffffff802bd440 __rt_init_lwip_system_init .rti_fn.3 0xffffffff802bd448 0x8 builddriversdrv_rtc.o 0xffffffff802bd448 __rt_init_rt_hw_rtc_init .rti_fn.3 0xffffffff802bd450 0x8 buildkernelcomponentsdriverssrcworkqueue.o 0xffffffff802bd450 __rt_init_rt_work_sys_workqueue_init .rti_fn.4 0xffffffff802bd458 0x8 builddrivers etsynopGMAC.o 0xffffffff802bd458 __rt_init_rt_hw_eth_init .rti_fn.4 0xffffffff802bd460 0x8 buildkernelcomponentsdfsfilesystemselmfatdfs_elm.o 0xffffffff802bd460 __rt_init_elm_init .rti_fn.4 0xffffffff802bd468 0x8 buildkernelcomponentslibccompilers ewliblibc.o 0xffffffff802bd468 __rt_init_libc_system_init .rti_fn.4 0xffffffff802bd470 0x8 buildkernelcomponents etsal_socketsrcsal_socket.o 0xffffffff802bd470 __rt_init_sal_init .rti_fn.6 0xffffffff802bd478 0x8 buildkernelcomponentsfinshshell.o 0xffffffff802bd478 __rt_init_finsh_system_init .rti_fn.6.end 0xffffffff802bd480 0x8 buildkernelsrccomponents.o 0xffffffff802bd480 __rt_init_rti_end 0xffffffff802bd488 __rt_init_end = . 0xffffffff802bd488 . = ALIGN (0x4) 0xffffffff802bd488 . = ALIGN (0x4)
實(shí)際上在執(zhí)行的時(shí)候,實(shí)現(xiàn)如下
/** * RT-Thread Components Initialization for board */void rt_components_board_init(void){#if RT_DEBUG_INIT int result; const struct rt_init_desc *desc; for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++) { rt_kprintf("initialize %s", desc->fn_name); result = desc->fn(); rt_kprintf(":%d done ", result); }#else volatile const init_fn_t *fn_ptr; for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++) { (*fn_ptr)(); }#endif}
并不是訪問的具體的函數(shù),而是從__rt_init_rti_board_start指向的指針開始,不停向下執(zhí)行,直到__rt_init_rti_board_end結(jié)尾。這樣就不依賴于具體的函數(shù)的實(shí)現(xiàn)了。所以函數(shù)的擴(kuò)展性非常好。
4.總結(jié)
以上介紹了linker script的原理,以及在實(shí)際使用過程中的幾個使用的技巧。這些都是在實(shí)際的項(xiàng)目中總結(jié)的來的,其實(shí)理解了linker script將可用完成很多有趣的使用技巧。只是平時(shí)我們并沒有特別關(guān)注這個文件的使用,也并沒有實(shí)際去編寫一個linker script完成一個工程的構(gòu)建。關(guān)于linker script的語法和使用,還有很多可以自由發(fā)揮的地方。
-
Linker
+關(guān)注
關(guān)注
0文章
3瀏覽量
1661
原文標(biāo)題:鏈接腳本linker script的妙用
文章出處:【微信號:Embeded_IoT,微信公眾號:嵌入式IoT】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論