在编译出 vmlinux 之前,构建系统会先链接出 vmlinux.o,但这一步骤的目的并不是要将 vmlinux.o 链接进 vmlinux 中。在链接 vmlinux.o 过程中主要做两件事情:
1. ELF section (节区) 的 mismatch 检查;
2. 生成内核导出符号文件 Module.symvers (Symbol version dump文件)
下面结合 modpost 工具的源码文件 modpost.c 粗略分析一下这两个过程:
在 http://www.groad.net/bbs/read.php?tid-4190.html 这篇文章的最后提到 modpost 命令的执行为:scripts/mod/modpost -o /home/beyes/Downloads/linux-2.6.35.13/Module.symvers -S vmlinux.o 在上面命令中,设置了 -S 选项后,sec_mismatch_verbose 变量就会被设置为 0,这表示在发生 mismatch 时,不会打印出详细的说明信息,这时往往会看到如下的提示:WARNING: modpost: Found 41 section mismatch(es).
To see full details build your kernel with:
'make CONFIG_DEBUG_SECTION_MISMATCH=y'
现在来到 modpost.c 中的 main() 函数,由于 kernel_read ,module_read 和 extsym_start 这 3 个变量都初始化为 NULL ,所以下面的程序片段不会执行:
[C++] 纯文本查看 复制代码 if (kernel_read)
read_dump(kernel_read, 1);
if (module_read)
read_dump(module_read, 0);
while (extsym_start) {
read_dump(extsym_start->file, 0);
extsym_iter = extsym_start->next;
free(extsym_start);
extsym_start = extsym_iter;
}
由于上面的命令只有一个选项 -S 及一个参数 vmlinux.o ,所以底下的 while() 里的 read_symbols() 函数只执行一次:
[C++] 纯文本查看 复制代码 while (optind < argc)
read_symbols(argv[optind++]);
下面看下 read_symbols() 函数,传进来的只有 vmlinu.o 这个参数。
进到 read_symbols() 函数中会首先执行 parse_elf() 函数,该函数用来对 ELF 文件做个基本的解析,vmlinux.o 也是 ELF 类型文件。在 parse_elf() 里:
[C++] 纯文本查看 复制代码 static int parse_elf(struct elf_info *info, const char *filename)
{
unsigned int i;
Elf_Ehdr *hdr;
Elf_Shdr *sechdrs;
Elf_Sym *sym;
hdr = grab_file(filename, &info->size); //hdr is a 'Elf32_Ehdr' struct pointer (elf.h)
if (!hdr) {
perror(filename);
exit(1);
}
... ...
在 grap_file() 函数里第一个参数即 vmlinux.o ,第 2 个参数用来存放 vmlinux.o 这个文件的大小。grap_file() 函数如下:
[C++] 纯文本查看 复制代码 void *grab_file(const char *filename, unsigned long *size)
{
struct stat st;
void *map;
int fd;
fd = open(filename, O_RDONLY);
if (fd < 0 || fstat(fd, &st) != 0)
return NULL;
*size = st.st_size;
map = mmap(NULL, *size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
if (map == MAP_FAILED)
return NULL;
return map;
}
上面的代码主要做两件事:
1. 返回 vmlinux.o 的大小到 info 结构中的 size 成员;
2. 将 vmlinux.o mmap ,然后返回映射后的内存指针。
再返回到 read_symbols() 中,下面的程序段用来判断 ELF 文件的头部 ident[] 数组里的信息:
[C++] 纯文本查看 复制代码 if ((hdr->e_ident[EI_MAG0] != ELFMAG0) ||
(hdr->e_ident[EI_MAG1] != ELFMAG1) ||
(hdr->e_ident[EI_MAG2] != ELFMAG2) ||
(hdr->e_ident[EI_MAG3] != ELFMAG3)) {
/* Not an ELF file - silently ignore it */
return 0;
}
上面就是判断头部是不是以固定的 " 0x7f, 'E', 'L', 'F' " 这几个字符开始,如果不是,那么表明不是 ELF 文件。
接下来的一连串赋值都是根据平台的不同(数据的大小端)而进行相关转换,这些数据都是 ELF 的头部数据:
[C++] 纯文本查看 复制代码 /* Fix endianness in ELF header */
hdr->e_type = TO_NATIVE(hdr->e_type);
hdr->e_machine = TO_NATIVE(hdr->e_machine);
hdr->e_version = TO_NATIVE(hdr->e_version);
hdr->e_entry = TO_NATIVE(hdr->e_entry);
hdr->e_phoff = TO_NATIVE(hdr->e_phoff);
hdr->e_shoff = TO_NATIVE(hdr->e_shoff);
hdr->e_flags = TO_NATIVE(hdr->e_flags);
hdr->e_ehsize = TO_NATIVE(hdr->e_ehsize);
hdr->e_phentsize = TO_NATIVE(hdr->e_phentsize);
hdr->e_phnum = TO_NATIVE(hdr->e_phnum);
hdr->e_shentsize = TO_NATIVE(hdr->e_shentsize);
hdr->e_shnum = TO_NATIVE(hdr->e_shnum);
hdr->e_shstrndx = TO_NATIVE(hdr->e_shstrndx);
再继续往下:
[C++] 纯文本查看 复制代码
sechdrs = (void *)hdr + hdr->e_shoff;
info->sechdrs = sechdrs;
/* Check if file offset is correct */
if (hdr->e_shoff > info->size) {
fatal("section header offset=%lu in file '%s' is bigger than "
"filesize=%lu\n", (unsigned long)hdr->e_shoff,
filename, info->size);
return 0;
}
上面,hdr 指向 vmlinux.o mmap() 出来后的空间的起始地址,e_shoff 用来指向 “节区头部表格”( sections header table );if 判断里检查这个偏移是否出错,正常情况下编译出来的 vmlinux.o 不会导致这样的错误,如若出错则返回退出。
继续,
[C++] 纯文本查看 复制代码 for (i = 0; i < hdr->e_shnum; i++) {
sechdrs[i].sh_name = TO_NATIVE(sechdrs[i].sh_name);
sechdrs[i].sh_type = TO_NATIVE(sechdrs[i].sh_type);
sechdrs[i].sh_flags = TO_NATIVE(sechdrs[i].sh_flags);
sechdrs[i].sh_addr = TO_NATIVE(sechdrs[i].sh_addr);
sechdrs[i].sh_offset = TO_NATIVE(sechdrs[i].sh_offset);
sechdrs[i].sh_size = TO_NATIVE(sechdrs[i].sh_size);
sechdrs[i].sh_link = TO_NATIVE(sechdrs[i].sh_link);
sechdrs[i].sh_info = TO_NATIVE(sechdrs[i].sh_info);
sechdrs[i].sh_addralign = TO_NATIVE(sechdrs[i].sh_addralign);
sechdrs[i].sh_entsize = TO_NATIVE(sechdrs[i].sh_entsize);
}
在上面代码中,e_shnum 成员表示节区头部表里条目个数,换句话来说就是有多少个节区。这里遍历所有节区头部,主要做的事情仍然是根据平台的不同对数据进行相应的修正。一般情况下,在 x86 中,TO_NATIVE 宏什么都不做,那么在此就只是表示简单的一个赋值。
继续往下:
[C++] 纯文本查看 复制代码
/* Find symbol table. */
for (i = 1; i < hdr->e_shnum; i++) {
const char *secstrings
= (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;
const char *secname;
int nobits = sechdrs[i].sh_type == SHT_NOBITS;
if (!nobits && sechdrs[i].sh_offset > info->size) {
fatal("%s is truncated. sechdrs[i].sh_offset=%lu > "
"sizeof(*hrd)=%zu\n", filename,
(unsigned long)sechdrs[i].sh_offset,
sizeof(*hdr));
return 0;
}
/*从节区里取出该节区的名字;sh_name 在节区头部表中,
是节区名字字符串的索引*/
secname = secstrings + sechdrs[i].sh_name;
if (strcmp(secname, ".modinfo") == 0) {
if (nobits)
fatal("%s has NOBITS .modinfo\n", filename);
info->modinfo = (void *)hdr + sechdrs[i].sh_offset;
info->modinfo_len = sechdrs[i].sh_size;
} else if (strcmp(secname, "__ksymtab") == 0)
info->export_sec = i;
else if (strcmp(secname, "__ksymtab_unused") == 0)
info->export_unused_sec = i;
else if (strcmp(secname, "__ksymtab_gpl") == 0)
info->export_gpl_sec = i;
else if (strcmp(secname, "__ksymtab_unused_gpl") == 0)
info->export_unused_gpl_sec = i;
else if (strcmp(secname, "__ksymtab_gpl_future") == 0)
info->export_gpl_future_sec = i;
/*SHT_SYMTAB表示此节区包含一个符号表,该类型用于链接编辑(ld)*/
/*不是 symtab 则继续查找*/
if (sechdrs[i].sh_type != SHT_SYMTAB)
continue;
/*找到symtab 或者是到达最后一个section 都没找到,这里是找到了symtab*/
/* sh_offset 节区的第一个字节与文件头之间的偏移*/
info->symtab_start = (void *)hdr + sechdrs[i].sh_offset;
info->symtab_stop = (void *)hdr + sechdrs[i].sh_offset
+ sechdrs[i].sh_size;
/* sh_link 头部表索引,链接到另一个section,这里是strtab */
/* sh_link 的值根据 sh_type 来决定。当sh_type 为 SHT_SYMTAB 时,sh_link由操作系统来指定 */
info->strtab = (void *)hdr +
sechdrs[sechdrs[i].sh_link].sh_offset; //获得strtab 节区指针
}
上面代码主要用来查找 symtab 这个节区。symtab 节区。symtab 称之为 “符号表” (Symbol Table)。在目标文件中,符号表用来定位,重定位程序中符号定义以及引用的信息。符号表的整体结构是一个 Elf32_Sym 的结构体数组,Elf32_Sym 结构定义在 elf.h 文件中,如下表示:
[C++] 纯文本查看 复制代码 /* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
在查找 symtab 的代码块中,secstrings 变量指向 .shstrtab 节区首字节,.shstrtab 节区存储着各个节区的名字,在 http://www.groad.net/bbs/read.php?tid-4228.html 这里有一个遍历 .shstrtab 节区并输出各个节区符号的例子。
节区头部有一个 sh_type 成员,它表示对应的节区类型,比如众所周知的 .bss 节区的类型为 SHT_NOBITS 。另外,节区头部还有一个成员 sh_name, 它是节区名字字符串表( .strshtab )的索引。所以由此看出,secname 变量就是用来存放节区的名字的。
在 vmlinu.o 中没有 .modinfo 这个节区,所以上面的 if 判断不成立。通过 readelf 工具辅助可以看到,默认配置的 vmlinu.o 里存在 __ksymtab 和 __ksymtab_gpl 这两个节区,所以 info->export_sec = i; 和 info->export_gpl_sec = i; 分别将这两个节区的索引记录下来,以备后用。
在 if (sechdrs.sh_type != SHT_SYMTAB) 里判断是否为 symtab 节区,不是的话则跳过。若遇到 symtab 节区,那么将节区的头头部和尾部的地址分别记录在 symtab_start 和 symtab_stop 这两个指针中。
info->strtab 是 strtab 节区指针。strtab 节区从结构上来讲是一个字符串数组。它和 symtab 节区是紧密相关的,可以通过 symtab 节区里的相关索引成员定位到 symtab 中,找到相应的符号名。在 http://www.groad.net/bbs/read.php?tid-4228.html 这里也有一个相应的实例。
到此已经分析完 parse_elf() 这个函数的作用。 |