在http://blog.csdn.net/flydream0/article/details/8684811一文中已對rrt-thread的moudle源碼進行分析,在講到rt_module_load函數時,程式將對兩個類型的elf檔案進行分別處理,共用目標檔案將用_load_shared_object函數來處理,而可重定位檔案則用_load_relocated_object函數來處理,由於要瞭解這兩個函數的具體過程,必須先熟悉瞭解elf檔案格式(參考:http://blog.csdn.net/flydream0/article/details/8719036)。這裡假設你已非常瞭解elf檔案格式的前提下說深入分析_load_shared_object函數的源碼,讓你了徹底瞭解rt-thread是如何輸入一個共用目標檔案。
_load_relocated_object函數大體分為四個步驟:
- 分配記憶體並初始化。
- 根據輸入目標檔案的順序,以段(segment)為單位將它們拼裝起來並映射到module->module_space記憶體空間。
- 對module->module_space的內容某些位置根據重定位資訊進行修改。
- 產生最終的符號表。
1 分析記憶體並初始化
static struct rt_module *_load_shared_object(const char *name,void *module_ptr){ rt_uint8_t *ptr = RT_NULL; rt_module_t module = RT_NULL; rt_bool_t linked = RT_FALSE; rt_uint32_t index, module_size = 0; RT_ASSERT(module_ptr != RT_NULL); //檢查elf檔案標識是否為"\177RTM",表示此elf檔案還未連結過的,則置linked標識,此標識在重定位過程中將會用到, if (rt_memcmp(elf_module->e_ident, RTMMAG, SELFMAG) == 0) { /* rtmlinker finished */ linked = RT_TRUE; } /* get the ELF image size *///計算elf記憶體鏡像大小 for (index = 0; index < elf_module->e_phnum; index++) { if (phdr[index].p_type == PT_LOAD) module_size += phdr[index].p_memsz; } if (module_size == 0) { rt_kprintf("Module: size error\n"); return RT_NULL; } /* allocate module *///動態分配一個rt_moudule對象 module = (struct rt_module *)rt_object_allocate(RT_Object_Class_Module, name); if (!module) return RT_NULL; //模組被引用次數初始化為0 module->nref = 0; /* allocate module space *///根據前面計算出來的記憶體鏡像大小動態分配一片記憶體,由module->module_space指向它。 module->module_space = rt_malloc(module_size); if (module->module_space == RT_NULL) { rt_kprintf("Module: allocate space failed.\n"); rt_object_delete(&(module->parent)); return RT_NULL; } /* zero all space */ ptr = module->module_space;//將這片記憶體全部清零. rt_memset(ptr, 0, module_size);
2 拼裝段到記憶體鏡像
接下來就是將所有類型為PT_LOAD的段拼裝到記憶體鏡像:
for (index = 0; index < elf_module->e_phnum; index++)//掃描所有段 { if (phdr[index].p_type == PT_LOAD)//如果段類型為PT_LOAD { rt_memcpy(ptr + phdr[index].p_paddr, //複製段的內容到記憶體錯鏡像 (rt_uint8_t *)elf_module + phdr[index].p_offset, phdr[index].p_filesz); } } /* set module entry *//設定模組的線程入口地址 module->module_entry = module->module_space + elf_module->e_entry;
以上代碼是掃描所有程式頭部表項,如果發現對應的段類型為PT_LOAD,則將其段內容原樣拷貝到記憶體鏡像中,段的地址由p_offset給出,記憶體鏡像地址由p_paddr給出位移地址,大小由p_filesz給出。
這裡要注意地是,p_filesz可能比p_memsz小,比如.bss段,但由於之前已全部清零,所以記憶體相對應的鏡像若無資料拷貝進來,則已全部清零,這個相當於初始於初始化,比如一些未初始化的全部變數。
3 根據重定位資訊對記憶體鏡像進行修改
for (index = 0; index < elf_module->e_shnum; index ++)//掃描所有節區頭部表項 { rt_uint32_t i, nr_reloc; Elf32_Sym *symtab; Elf32_Rel *rel; rt_uint8_t *strtab; static rt_bool_t unsolved = RT_FALSE; if (!IS_REL(shdr[index]))//過濾掉非重定位類型的節區 continue; /* get relocate item *///擷取重定位項的資料結構 rel = (Elf32_Rel *)((rt_uint8_t *)module_ptr + shdr[index].sh_offset); /* locate .rel.plt and .rel.dyn section */ symtab = (Elf32_Sym *)((rt_uint8_t *)module_ptr + //sh_link在節區類型為重定位類型時含義為指向符號表類型的節區頭部表項 shdr[shdr[index].sh_link].sh_offset); strtab = (rt_uint8_t *)module_ptr + //此時符號表類型的節區頭號部表項的sh_link指向字串類型的節區頭部表項 shdr[shdr[shdr[index].sh_link].sh_link].sh_offset; nr_reloc = (rt_uint32_t)(shdr[index].sh_size / sizeof(Elf32_Rel));//得出有多少個重定位項資料結構 /* relocate every items */ for (i = 0; i < nr_reloc; i ++)//掃描此節區所包含的每個重定位元據結構 { Elf32_Sym *sym = &symtab[ELF32_R_SYM(rel->r_info)];//rel->r_info的高8位表示符號表的索引值,這行代碼返回一符號表項 RT_DEBUG_LOG(RT_DEBUG_MODULE, ("relocate symbol %s shndx %d\n",//調試列印 strtab + sym->st_name, sym->st_shndx)); if ((sym->st_shndx != SHT_NULL) || //如果符號表項關聯的節區頭部表項不為空白或符號表項類型為本地符號 (ELF_ST_BIND(sym->st_info) == STB_LOCAL)) { rt_module_arm_relocate(module, rel, //則根據重定位資訊修改記憶體鏡像中的內容 (Elf32_Addr)(module->module_space + sym->st_value)); } else if (!linked)//如果還未鏈表 { Elf32_Addr addr; RT_DEBUG_LOG(RT_DEBUG_MODULE, ("relocate symbol: %s\n", strtab + sym->st_name)); /* need to resolve symbol in kernel symbol table */ addr = rt_module_symbol_find((const char *)(strtab + sym->st_name));//在核心中根據符號表項給出的名字尋找模組 if (addr == 0) { rt_kprintf("Module: can't find %s in kernel symbol table\n", strtab + sym->st_name); unsolved = RT_TRUE;//未解決標誌 } else rt_module_arm_relocate(module, rel, addr);//根據在核心中尋找對應的模組進行重定位 } rel ++; } if (unsolved)//如果最終未解決,則刪除此模組,然後返回 { rt_object_delete(&(module->parent)); return RT_NULL; } }
上述代碼中,程式將對每個類型為重定位節區的節區頭號部表項進行操作,根據上一章http://blog.csdn.net/flydream0/article/details/8719036內容的2.3.1.3節內容可知,類型為重定位的節區頭部表的sh_offset指向重定位項的資料結構,從sh_size得出共有多少個重定位項,此時sh_link指向另一個節區頭部表項,此節區頭部表項表示一符號表,其sh_offset指向的是符號表的地址位置,其sh_link指向的是字串表地址。主要是得到這三個資訊。
然後根據重定位項內的資料定位到具體是哪個符號需要重定位,其中重定位項的資料結構如下定義:
typedef struct elf32_sym{ Elf32_Word st_name; //符號名 Elf32_Addr st_value; //符號值 Elf32_Word st_size; //符號大小 unsigned char st_info; //符號類型及綁定屬性(見上一博文的2.5.2節) unsigned char st_other; //無含義 Elf32_Half st_shndx; //此符號關聯的節區頭部表項索引} Elf32_Sym;
其中符號表項的st_info的高四位表示符號綁定屬性,低四位表示符號類型,具體參見http://blog.csdn.net/flydream0/article/details/8719036#t10的2.5.1節。
接下來判斷此符號關聯的節區不為空白或此符號為本地符號,這樣一來就可以根據此節區來進行重定位了,重定位的操作是通過rt_module_arm_relocate函數來完成的,它是一個與ARM架構機器相關的函數,它需要傳入三個參數,模組地址,重定位元據結構,模組記憶體鏡像的具體節區位置(由符號表項的st_value值給出,參見前一文的2.5.3節),通過這三個參數,就可以修改記憶體鏡像的具體位置了,由於rt_module_arm_relocate函數與具體的ARM機器太緊密相關,這裡作詳細介紹無太大意義,所以跳過,讀者如果興趣找相關資料自行研究。
如果上述條件不滿足,接下來若是此elf檔案未連結,則表示此符號可以通過其它模組的資訊來進行連結並重定位對應的記憶體鏡像位置,所以程式會根據名稱到核心尋找到對應的模組,然後進行重定位操作。
4 產生最終的符號表
rt-thread在產生符號表之前首先尋找名為“.dynsym”的節區,這個節區是特殊節區,見前一文的2.3.2節內容,它包含了動態鏈表符號表。
尋找.dynsym節區:
for (index = 0; index < elf_module->e_shnum; index ++)//掃描所有節區頭部表項 { /* find .dynsym section */ rt_uint8_t *shstrab; shstrab = (rt_uint8_t *)module_ptr + shdr[elf_module->e_shstrndx].sh_offset;//定位字串表 if (rt_strcmp((const char *)(shstrab + shdr[index].sh_name), ELF_DYNSYM) == 0)//判斷此節區是否名為“.dynsym” break; }
接下來判斷此節區包含多少個全域符號,然後為它申請記憶體空間:
if (index != elf_module->e_shnum) { int i, count = 0; Elf32_Sym *symtab = RT_NULL; rt_uint8_t *strtab = RT_NULL; symtab =(Elf32_Sym *)((rt_uint8_t *)module_ptr + shdr[index].sh_offset);//得到此節區符號表 strtab = (rt_uint8_t *)module_ptr + shdr[shdr[index].sh_link].sh_offset;//字串表 for (i=0; i<shdr[index].sh_size/sizeof(Elf32_Sym); i++)//掃描所有符號表項,得到為全域函數符號的個數,即匯出的函數名符號 { if ((ELF_ST_BIND(symtab[i].st_info) == STB_GLOBAL) && (ELF_ST_TYPE(symtab[i].st_info) == STT_FUNC)) count ++; } module->symtab = (struct rt_module_symtab *)rt_malloc//為模組分配符號表記憶體空間 (count * sizeof(struct rt_module_symtab)); module->nsym = count;//設定符號個數
最後逐個將符號拷貝:
for (i=0, count=0; i<shdr[index].sh_size/sizeof(Elf32_Sym); i++)掃描所有匯出符號 { rt_size_t length; if ((ELF_ST_BIND(symtab[i].st_info) != STB_GLOBAL) || (ELF_ST_TYPE(symtab[i].st_info) != STT_FUNC)) continue; length = rt_strlen((const char *)(strtab + symtab[i].st_name)) + 1;//符號名長度 module->symtab[count].addr = (void *)(module->module_space + symtab[i].st_value);//設定符號地址 module->symtab[count].name = rt_malloc(length);//設定符號名 rt_memset((void *)module->symtab[count].name, 0, length); rt_memcpy((void *)module->symtab[count].name, strtab + symtab[i].st_name, length); count ++; }
從上面可以看出,匯出符號表的過程其實是找出.dynsym節區,因為此節區包含了所有鏈表符號,但並是不所有符號都是需要匯出的,只在標誌為全域的符號,且此符號類型為函數的符號才是需要匯出的。最後將此類型的全域函數全部匯出到模組對應的符號表內。
rt-thread裝載共用檔案的過程分析完畢!