rt-thread装载共享目标文件的过程源码分析

在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函数大体分为四个步骤:

  1. 分配内存并初始化。
  2. 根据输入目标文件的顺序,以段(segment)为单位将它们拼装起来并映射到module->module_space内存空间。
  3. 对module->module_space的内容某些位置根据重定位信息进行修改。
  4. 生成最终的符号表。

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装载共享文件的过程分析完毕!


你可能感兴趣的:(rt-thread装载共享目标文件的过程源码分析)