kallsyms在进行源码调试时具有相当重要的作用。linux内核在编译的过程中,将内核中所有的符号(所有的内核函数以及已经装载的模块)及符号的地址以及符号的类型信息都保存在了/proc/kallsyms文件中。具体格式如下:符号地址,符号类型,符号名
c0100000 T startup_32
c0100000 A _text
c01000c6 t checkCPUtype
c0100147 t is
486c010014e t is
386c010019f t L
6c01001a1 t check_x
87c01001ca t setup_idt
c01001e7 t rp_sidt
c01001f4 t ignore_int
c0100228 T calibrate_delay为了弄清楚内核具体是怎样查询的,我们首先必须知道这些信息在内核中是怎样存储的。
内核为了存储这些信息,定义了如下变量:
extern const unsigned long kallsyms_num_symsextern const u8 kallsyms_token_table[] const u16 kallsyms_token_index[] extern const unsigned long kallsyms_addresses[] extern const unsigned long kallsyms_markers[] |
其中kallsyms_num_syms统计了内核中所有符号的个数;
kallsyms_addresses数组记录了所有内核符号的地址(已经按顺序排列好)
kallsyms_names数组是函数名组成的一个大串,而该串的具体格式为
<lenth><length byte dada>
也就是说对于每个符号而言,都存储的是length+1个字节的内容,其中第一个字节代表了该符号的总长度,而后面length个自己代表了符号的内容。
在这里需要说明一下为了方便符号的查找,linux内核做了两方面的工作:
将常用的串存储在数组kallsyms_token_table数组中,而kallsyms_token_index则代表了该串的索引。其中token就代表了这些常用的字符串,而索引就是一些没有用到的ASCII码值。其中的一些例子入表1所示
190 .asciz "t.text.lock."
191 .asciz "text.lock."
192 .asciz "t.lock."
193 .asciz "lock."
210 .asciz "tex"
229 .asciz "t."
239 .asciz "loc"
249 .asciz "oc"
250 .asciz "te"
下面我们来具体分析内核和源码的实现(linux-3.5.4)
static unsigned int kallsyms_expand_symbol(unsigned int off, char *result) { int len, skipped_first = 0; const u8 *tptr, *data; /* Get the compressed symbol length from the first symbol byte. */ data = &kallsyms_names[off]; //符号的第一个字符就是符号的长度,在这里赋值给了变量len。 len = *data; //将data指向了符号的下一个字节,这就是符号真正的内容了。 data++; /* * Update the offset to return the offset for the next symbol on * the compressed stream. */ off += len + 1; //下面这句话乍看有点费解。前面我们介绍过,为了提高查询速度,我们将常用的字符串存储在kallsyms_token_table中,kallsyms_token_index记录每个ascii字符的替代串在kallsyms_token_table中的偏移 //比如我们需要查找的符号信息是 0x03, 0xbe, 0xbc, 0x71其中3代表了符号的长度,而后面紧跟的三个字节就是符号的内容了。我们需要知道be究竟代表的是什么串。我们首先需要通过 //kallsyms_token_index[0xbe]查到0xbe所对应的串在kallsyms_token_table中的索引,然后将该串的首地址赋给tptr。从表1中我们查到0xbe(190)所对应的串为 "t.text.lock."其中第一个字母t代表了符号的类型 //然后data指向下一个要解析的字节0xbc。 while (len) { tptr = &kallsyms_token_table[kallsyms_token_index[*data]]; data++; len--; //跳过第一个字符,因为它不是真正的符号的内容,而是符号的类型。将该字符串赋给了result。然后依次解析每个数据,这样,result中就存放了符号的名字了。至此,整个解析过程就结束了。而此时off返回的是下一个符号在 //kallsyms_names中的索引 while (*tptr) { if (skipped_first) { *result = *tptr; result++; } else skipped_first = 1; tptr++; } } *result = '\0'; /* Return to offset to the next symbol. */ return off; }
const char *kallsyms_lookup(unsigned long addr,unsigned long *symbolsize,unsigned long *offset,char **modname, char *namebuf) { namebuf[KSYM_NAME_LEN - 1] = 0; namebuf[0] = 0; if (is_ksym_addr(addr)) { unsigned long pos; //通过对get_symbol_pos函数的分析,pos代表了找到的匹配的地址在地址表中kallsym_addresses数组中的索引(此处为5) pos = get_symbol_pos(addr, symbolsize, offset); /* Grab name */ ///该函数调用完成之后,符号的名称就存放在了namebuf中,并且返回,符号解析结束了。 kallsyms_expand_symbol(get_symbol_offset(pos), namebuf); if (modname) *modname = NULL; return namebuf; } /* See if it's in a module. */ return module_address_lookup(addr, symbolsize, offset, modname, namebuf); }
其中,参数addr代表了我们需要查询的符号的地址。我们这里假设现在的符号表中已有的地址为下列序列,而我们所要查询的是地址9.(显然在符号表中没有改地址)
说明一下:符号的地址都是32位的,我在这里只是为了方便说明而做的假设。假设某些符号的地址为下面这些数字。从下面的假设情况来看,
kallsyms_num_syms=9;即共有七个符号
kallsyms_addresses数组中显然存储了线面这些地址,
kallsyms_names存储了函数名组成的大串。
0 1 2 5 7 8 8 8 10 |
symbolsize,offset,modname,namebuf。
在kallsyms_lookup函数中首先调用了get_symbol_pos(addr, symbolsize, offset)函数获取该符号的偏移量。我们下面分析一下这个函数。
//从这里我们可以看出kallsyms_lookup中获悉的symbolsize,offset信息就是由get_symbol_pos函数返回的。 static unsigned long get_symbol_pos(unsigned long addr,unsigned long *symbolsize,unsigned long *offset) { unsigned long symbol_start = 0, symbol_end = 0; unsigned long i, low, high, mid; /* This kernel should never had been booted. */ BUG_ON(!kallsyms_addresses); //从这里我们看到:对符号的查询是由的是折半查找的方式,此时显然high=8,low=0(我们都使用上面假设的条件进行分析) /* Do a binary search on the sorted kallsyms_addresses array. */ low = 0; high = kallsyms_num_syms; while (high - low > 1) { mid = low + (high - low) / 2; if (kallsyms_addresses[mid] <= addr) low = mid; else high = mid; } /* * Search for the first aliased symbol. Aliased * symbols are symbols with the same address. */ ///显然此处是对重复地址所做的工作 while (low && kallsyms_addresses[low-1] == kallsyms_addresses[low]) --low; //当返回之后,low=5,high=8 symbol_start = kallsyms_addresses[low]; //symbol_start=8;而symbol_end=10 /* Search for next non-aliased symbol. */ for (i = low + 1; i < kallsyms_num_syms; i++) { if (kallsyms_addresses[i] > symbol_start) { symbol_end = kallsyms_addresses[i]; break; } } /* If we found no next symbol, we use the end of the section. */ if (!symbol_end) { if (is_kernel_inittext(addr)) symbol_end = (unsigned long)_einittext; else if (all_var) symbol_end = (unsigned long)_end; else symbol_end = (unsigned long)_etext; } //symbol_size=2 if (symbolsize) *symbolsize = symbol_end - symbol_start; //offset=1;在这里我们就明白了offset的含义就是我们所需查找的地址和小于该地址的符号表地址之间的差值。(前提条件是该地址在符号表中不存在) if (offset) *offset = addr - symbol_start; //此时low=5; return low; } 其次调用了kallsyms_expand_symbol(get_symbol_offset(pos), namebuf);而这个函数的参数中又调用了 get_symbol_offset(pos)函数,下面我们先分析 get_symbol_offset(pos)函数。 static unsigned int get_symbol_offset(unsigned long pos) { const u8 *name; int i; /* * Use the closest marker we have. We have markers every 256 positions, * so that should be close enough. */ name = &kallsyms_names[kallsyms_markers[pos >> 8]];/*上面这个name = &kallsyms_names[kallsyms_markers[pos >> 8]];语句似乎有点难懂;因为在这里,linux内核为了便于查询,将kallsyms_names将符号每256个分为一组,而这个pos前面我们已经分析过了,就是符号地址。在kallsyms_address中的索引。那么显然pos>>8就代表了该符号位于kallsyms_names中的哪一组了。 而kallsyms_markers这个数组中的每个元素分别代表了该组的字符串在kallsyms_names中的偏移量。那上面这句话就不难理解了。首先。我们通过pos查到该符号所属的组;然后通过kallsyms_markers数组查询到该组的字符串在kallsyms_names中的索引。那么显然那么就是该字符串的首地址。也就是说,通过name = &kallsyms_names[kallsyms_markers[pos >> 8]];name就指向了该pos所在的组的第一个字符。显然pos并不一定就是改组的第一个符号,那么接下来,我们就要从第一个符号起,开始查找,直到找到pos这个符号为止。获取到这个索引之后,就要对符号进行解析了。这就是紧接着的for循环干的事情了。
前面我们已经介绍过了,每个符号由length+1个字节组成其中第一个自己就是该符号的长度,接下来的length就是符号的内容。显然for循环所做的工作就是一次读取每个符号的第一个字符,获取该符号的长度,然后name向后移动*name+1个字节,这样就指向了下一个符号知道找到pos所对应的符号为止。该函数返回的是pos所对应的符号在kallsyms_name中的索引。*/ for (i = 0; i < (pos & 0xFF); i++) name = name + (*name) + 1; return name - kallsyms_names; }