一、前言
Linux内核是一个整体结构,而模块是插入到内核中的插件。尽管内核不是一个可安装模块,但为了方便起见,Linux把内核也看作一个模块。那么模块与模块之间如何进行交互呢,一种常用的方法就是共享变量和函数。但并不是模块中的每个变量和函数都能被共享,内核只把各个模块中主要的变量和函数放在一个特定的区段,这些变量和函数就统称为符号。
因此,内核也有一个module结构,叫做kernel_module。另外,从kernel_module开始,所有已安装模块的module结构都链在一起成为一条链,内核中的全局变量module_list就指向这条链:
struct module *module_list = &kernel_module;
一般来说,内核只会导出由EXPORT_PARM宏指定的符号给模块使用。为了使debugger提供更好的调试功能,需要使用kallsyms工具为内核生成__kallsyms段数据,该段描述所有不处在堆栈上的内核符号。这样debugger就能更好地解析内核符号,而不仅仅是内核指定导出的符号。
二、简介
在v2.6.0 的内核中,为了更好地调试内核,引入新的功能kallsyms.kallsyms把内核用到的所有函数地址和名称连接进内核文件,当内核启动后,同时加载到内存中.当发生oops,例如在内核中访问空地址时,内核就会解析eip位于哪个函数中,并打印出形如:
EIP is at cleanup_module+0xb/0x1d [client]的信息,
调用栈也用可读的方式显示出来.
Call Trace:
[<c013096d>] sys_delete_module+0x191/0x1ce
[<c02dd30a>] do_page_fault+0x189/0x51d
[<c0102bc1>] syscall_call+0x7/0xb
当然功能不仅仅于此,还可以查找某个函数例如的sys_fork的地址,然后hook它,kprobe就是这么干的。在v2.6.20 中,还可以包含所有符号的地址,应此功能更强大,就相当于内核中有了System.map了,此时查找sys_call_table的地址易如反掌。
三.sym的生成
1.形成过程
Linux内核符号表/proc/kallsyms的形成过程
(1)./scripts/kallsyms.c负责生成System.map
(2)./kernel/kallsyms.c负责生成/proc/kallsyms
(3)./scripts/kallsyms.c解析vmlinux(.tmp_vmlinux)生成kallsyms.S(.tmp_kallsyms.S),然后内核编译过程中将kallsyms.S(内核符号表)编入内核镜像uImage.内核启动后./kernel/kallsyms.c解析uImage形成/proc/kallsyms
2.内核配置
在2.6 内核中,为了更好地调试内核,引入了kallsyms。kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进kernel p_w_picpath,相当于内核中存了一个System.map。需要配置CONFIG_KALLSYMS。
.config
CONFIG_KALLSYMS=y 符号表中包含所有的函数
CONFIG_KALLSYMS_ALL=y 符号表中包括所有的变量(包括没有用EXPORT_SYMBOL导出的变量)
CONFIG_KALLSYMS_EXTRA_PASS=y
make menuconfig
General setup --->
[*] Configure standard kernel features (for small systems) --->
[*] Load all symbols for debugging/ksymoops (选中此项,才有/proc/kallsyms接口文件, oops问题,选中此选项即可,子选项可以忽略)
[*] Include all symbols in kallsyms
[*] Do an extra kallsyms pass
3.编译生成列表
内核编译的最后阶段,make会执行
nm -n vmlinux|scripts/kallsyms
nm -n vmlinux生成所有的内核符号,并按地址排序,形如
......
c0100000 T startup_32
c0100000 A _text
c01000c6 t checkCPUtype
c0100147 t is486
c010014e t is386
c010019f t L6
c01001a1 t check_x87
c01001ca t setup_idt
c01001e7 t rp_sidt
c01001f4 t ignore_int
c0100228 T calibrate_delay
c0100228 T stext
c0100228 T _stext
c010036b t rest_init
c0100410 t do_pre_smp_initcalls
c0100415 t run_init_process
......
v2.6.0 的行数是2.5万左右
4.处理列表
scripts/kallsyms则处理这个列表,并生成连接所需的S文件kallsyms.S。在linux3.12中使用/scripts/kallsyms处理此列表。v2.6.0中形如:
#include <asm/types.h>
#if BITS_PER_LONG == 64
#define PTR .quad
#define ALGN .align 8
#else
#define PTR .long
#define ALGN .align 4
#endif
.data
.globl kallsyms_addresses
ALGN
kallsyms_addresses:
PTR 0xc0100228
PTR 0xc010036b
PTR 0xc0100410
PTR 0xc0100415
PTR 0xc010043c
PTR 0xc0100614
...
.globl kallsyms_num_syms
ALGN
kallsyms_num_syms:
PTR 11228
.globl kallsyms_names
ALGN
kallsyms_names:
.byte 0x00
.asciz "calibrate_delay"
.byte 0x00
.asciz "stext"
.byte 0x00
.asciz "_stext"
...
生成的符号表部分如下:
/*
......
c1618b03 t __raw_write_unlock_irq.constprop.29
c1618b19 T panic
c1618c91 T printk
......
c16a4d6b r __func__.17404
c16a4d78 R kallsyms_addresses
c16ef0dc R kallsyms_num_syms
c16ef0e0 R kallsyms_names
c17d5468 R kallsyms_markers
c17d590c R kallsyms_token_table
c17d5c78 R kallsyms_token_index
......
*/
5.生成的符号数组解析
1)kallsyms_addresses数组包含所有内核函数的地址(经过排序的),v2.6.0 中相同的地址在kallsyms_addresses中只允许出现一次,到后面的版本例如相同的地址可以出现多次,这样就允许同地址函数名的出现。
例如:
kallsyms_addresses:
PTR 0xc0100228
PTR 0xc0100228
PTR 0xc0100228
PTR 0xc010036b
当查找某个地址时所在的函数时,v2.6.0 采用的是线性法,从头到尾地找,很低效,后来改成了折半查找,效率好多了。
2)kallsyms_num_syms是函数个数
3)kallsyms_names是函数名数组。
<1>以前的算法是:函数名数组组成的一个大串,这个大串是有许多小串组成,格式是:
.byte len
.asciz 压缩串
格式例如:
kallsyms_names:
.byte 0x00
.asciz "calibrate_delay"
.byte 0x00
.asciz "stext"
.byte 0x00
.asciz "_stext"
.byte 0x00
.asciz "rest_init"
len代表本函数名和前一函数名相同前缀的大小,例如
.byte 0x00
.asciz "early_param_test"
.byte 0x06
.asciz "setup_test"
.byte 0x06,说明串setup_test和串early_parm_test有着相同的前缀,长为6,即early_,所有setup_test最终解压后的函数名为early_setup_test.由于没有其他的辅助手段,函数名的解析过程也很低效,从头一直解析到该函数位置为止。
<2>在后来的版本中,算法有了改善,使用了偏移索引和高频字符串压缩。也就是现在常用的算法。格式是:
.byte len ascii字符 ascii字符...(len个ascii字符)
先建立token的概念,token就是所有函数名中,出现频率非常高的那些字符串.由于标识符命名
规则的限制,有许多ascii字符是未用到的,那么,可以用这些字符去替代这些高频串。例如下面的例子:
字符值 字符代表的串
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"
例如串.byte 0x03, 0xbe, 0xbc, 0x71的解析
串长3,
0xbe(190) .asciz "t.text.lock."
0xbc(189) .asciz "ir"
0x71(113) .asciz "q"
所以该串解析后的值是 t.text.lock.irq,注意实际的串值是.text.lock.irq,前面的t是类型,这是新版本加入的功能,将类型字符放在符号前。
.byte 0x02, 0x08, 0xc2
串长2,
0x08,8 .asciz "Tide_"
0xc2,194 .asciz "init"
所以该串解析后的值是 Tide_init,即ide_init
4)为了解析而设置了数据结构kallsyms_token_table和kallsyms_token_index.结构kallsyms_token_table记录每个ascii字符的替代串,kallsyms_token_index记录每个ascii字符的替代串在kallsyms_token_table中的偏移.
5)而数据结构的改变是,把函数名每256个分一组,用一个数组kallsyms_markers记录这些组在
kallsyms_names中的偏移,这样查找就方便多了,不必从头来。
四、符号表的查找
通过对以上格式的了解,我们就可以自己编写程序找到内核中的符号表,进而找到每个内核符号的地址。
1.首先找到kallsyms_addresses数组
//首先获得kallsyms_addresses数组中的printk的地址
printk_addr_addr = prink_addr_addr();
static void * prink_addr_addr(void){
unsigned int i = 0xc0000000;
int k = 0;
//kallsyms_addresses数组中都是保存的是内核符号的地址,所以这里查找的是地址下的保存的函数地址是否为printk的函数地址
for(;i < 0xf0000000; i += 4){
if(*((unsigned int *)i) == (unsigned int)printk){
//判断该地址前边都是有效的kallsyms_addresses数组函数地址
if(isfunaddr(*((unsigned int *)(i-4)))&&isfunaddr(*((unsigned int *)(i-8)))){
if(!k)
return (void *)i;
else
++k;
}
}
}
return NULL;
}
//只要该函数符号在kallsyms_addresses数组中,通过%Ps打印结构一定是...+0x0/...
static int isfunaddr(unsigned int addr){
char buff[200] = {0};
int i = 0;
memset(buff,0,sizeof(buff));
//get the %pS print format;
sprintf(buff,"%pS",(void *)addr);
//if is a function addr ,it's %pS print format must be: ...+0x0/...
if((buff[0]=='0')&&(buff[1]=='x'))
return 0;
while(buff[i++]){
if((buff[i] == '+')&&(buff[i+1]=='0')&&(buff[i+2]=='x')&&(buff[i+3]=='0')&&(buff[i+4]=='/'))
return 1;
}
return 0;
}
//通过printk的地址查找到kallsyms_addresses的结束地址kallsyms_addresses数组结尾处
funaddr_endaddr = find_funadd_endaddr(printk_addr_addr);
//一直循环查找最后一个符号不为...+0x0/...结构,即找到了
static void * find_funadd_endaddr(void * in){
unsigned int * p = in;
for(;isfunaddr(*p); ++p);
return (void *)(p-1);
}
//kallsyms_addresses数组尾地址+4就是符号个数kallsyms_num的地址,就能得到符号的个数
kallsyms_num = *((unsigned int *)funaddr_endaddr + 1);
//根据符号个数和kallsyms_addresses数组尾地址就能得到kallsyms_addresses数组的首地址
kallsyms_addr = (unsigned int *)funaddr_endaddr - kallsyms_num + 1;
2.找到kallsyms_name地址。
//kallsyms_num地址的下一项就是kallsyms_name地址
kallsyms_name = (void *)((unsigned int *)funaddr_endaddr + 2);
3.kallsyms_name数组的下一项是kallsyms_mark数组,但是他们的地址不是连续的。
//把函数名每256个分一组,用一个数组kallsyms_markers记录这些组在kallsyms_names中的偏移
kallsyms_mark = get_marker_addr(kallsyms_name );
//因为数组kallsyms_name中的格式是:.byte len ascii码...(len个)
static void * get_marker_addr(void * name){
int i = 0;
unsigned char * base = (char *)name;
//base[0]存的是.byte替代串的ascii码,base[1]存的是ascii码个数
//所以依次跳过kallsyms_num个name就找到了kallsyms_mark数组的地址
for(; i < kallsyms_num ; ++i){
base += (base[0] +1);
}
//4字节对齐
if((unsigned int)base%4){
base += (4-(unsigned int)base%4);
}
return (void *)base;
}
4.计算kallsyms_mark数组表项个数
//根据符号个数计算kallsyms_mark数组的个数,符号以256个为一组
mark_num = kallsyms_num%256 ? kallsyms_num/256 + 1:kallsyms_num/256;
5.获取结构kallsyms_token_table地址
//结构kallsyms_token_table记录每个ascii字符的替代串,位于kallsyms_mark数组之后
kallsyms_token_tab = (void *)((unsigned int *)kallsyms_mark + mark_num);
6.获得kallsyms_token_indx地址
//kallsyms_token_index记录每个ascii字符的替代串在kallsyms_token_table中的偏移.
kallsyms_token_indx = get_index_addr(kallsyms_token_tab);
//因为kallsyms_token_table里存放的都是字符类型,依次跳过合法的字符类型之后的地址就是kallsyms_token_indx地址
static void * get_index_addr(void * base){
char * p = (char *)base;
for(;is_funname_char(*p);p++){
for(;is_funname_char(*p); p++);
}
//align 4 bytes
if((unsigned int)p%4){
p += (4 -(unsigned int)p%4);
}
return (void *)p;
}
//检查是否为函数符号名的合法字符
static int is_funname_char( char p){
if( ((p >= 'a')&&(p <='z')) || ((p >= 'A')&&(p <='Z')) || (p == '_') || ( (p >='0')&&(p <= '9') ) || (p == '.'))
return 1;
else
return 0;
}
7.查找函数名地址
static void * name_2_addr(char * name){
char namebuff[200];
unsigned int i = 0;
unsigned int len = 0;
//符号名称数组
unsigned char * name_tab = (unsigned char *)kallsyms_name;
unsigned int mod_addr = 0;
char * buff_ptr = namebuff;
//遍历所有符号
for(; i < kallsyms_num; ++i){
memset(namebuff,0,200);
buff_ptr = namebuff;
len = *name_tab;//符号名称长度
name_tab++;//符号名对应的ascii码
while(len){
//根据符号名对应的ascii码得到符号名
buff_ptr = cp_from_token_tab(buff_ptr,
//得到该ascii码对应的字符串在kallsyms_token_table中的偏移
((unsigned short *)kallsyms_token_indx)[*name_tab]);
name_tab++;
len--;
}
//检查符号名是否一致,其中第一个字符为符号类型,忽略掉
if(my_strcmp(name,namebuff+1)==0){
//若相等返回该符号地址
return (void *)((unsigned int *)kallsyms_addr)[i];
}
}
}
static char * cp_from_token_tab(char * buff,unsigned short off)
{
int len = 0;
//从kallsyms_token_tab数组的偏移处取得字符串,字符串以“\0”隔开
char * token_tab = &(((char *)kallsyms_token_tab)[off]);
for(;token_tab[len]; ++len){
*buff = token_tab[len];
buff++;
};
return buff;
}
五、符号解析
//v2.6.20 当发生oops时,
fastcall void __kprobes do_page_fault(struct pt_regs *regs,unsigned long error_code)
{
...
die("Oops", regs, error_code);
...
}
void die(const char * str, struct pt_regs * regs, long err)
{
...
print_symbol("%s", regs->eip);//解析
...
}
static inline void print_symbol(const char *fmt, unsigned long addr)
{
__check_printsym_format(fmt, "");
__print_symbol(fmt, (unsigned long)__builtin_extract_return_addr((void *)addr));
}
void __print_symbol(const char *fmt, unsigned long address)
{
char buffer[KSYM_SYMBOL_LEN];
//取得该地址的符号信息,存入buffer中
sprint_symbol(buffer, address);
//将buffer中的符号信息打印出来
printk(fmt, buffer);
}
int sprint_symbol(char *buffer, unsigned long address)
{
return __sprint_symbol(buffer, address, 0, 1);
}
static int __sprint_symbol(char *buffer, unsigned long address,int symbol_offset, int add_offset)
{
char *modname;
const char *name;
unsigned long offset, size;
int len;
address += symbol_offset;//符号偏移是0
//解析地址,返回函数起始地址,大小,偏移,函数名
name = kallsyms_lookup(address, &size, &offset, &modname, buffer);
if (!name)
return sprintf(buffer, "0x%lx", address);
//先拷贝该地址对应的函数名给buffer[]
if (name != buffer)
strcpy(buffer, name);
len = strlen(buffer);
offset -= symbol_offset;
//将该函数符号的偏移地址和大小拷贝给buffer[]
if (add_offset)
len += sprintf(buffer + len, "+%#lx/%#lx", offset, size);
//若属于模块,则拷贝模块名给buffer[]
if (modname)
len += sprintf(buffer + len, " [%s]", modname);
return len;
}
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;
//取得符号的大小和偏移,返回符号在kallsyms_addresses数组中的索引值
pos = get_symbol_pos(addr, symbolsize, offset);
//解析符号,获得符号名称存入namebuf
kallsyms_expand_symbol(get_symbol_offset(pos),namebuf, KSYM_NAME_LEN);
if (modname)
*modname = NULL;
return namebuf;
}
//若不是内核符号,则扫描内核中已安装的模块中的符号
return module_address_lookup(addr, symbolsize, offset, modname,namebuf);
}
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);
low = 0;
//kallsyms_num_syms是内核函数个数
high = kallsyms_num_syms;
//折半查找,kallsyms_addresses数组包含所有内核函数的地址(经过排序的)
while (high - low > 1) {
mid = low + (high - low) / 2;
if (kallsyms_addresses[mid] <= addr)
low = mid;
else
high = mid;
}
//找到第一个对齐的符号,即相同地址中的第一个。v2.6.0中相同的地址在kallsyms_addresses中只允许出现一次,到后面的版本例如相同的地址可以出现多次,这样就允许同地址函数名的出现。
while (low && kallsyms_addresses[low-1] == kallsyms_addresses[low])
--low;
//获得函数地址小于addr最接近的一个内核函数的地址作为符号的起始地址
symbol_start = kallsyms_addresses[low];
//找到下一个不同的地址
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;
}
//获得符号的大小
if (symbolsize)
*symbolsize = symbol_end - symbol_start;
//符号的偏移量
if (offset)
*offset = addr - symbol_start;
//返回在kallsyms_addresses数组中的索引值
return low;
}
//返回符号在kallsyms_names中的偏移
static unsigned int get_symbol_offset(unsigned long pos)
{
const u8 *name;
int i;
//找到该组在kallsyms_names中的偏移。pos>>8即是pos/256得到kallsyms_markers的索引,kallsyms_markers数组中存储的是每256个分一组的组在kallsyms_names的偏移。
//kallsyms_names是函数名组成的一个大串,这个大串是有许多小串组成,格式是:
//.byte len ascii码 ascii码...(len个)
name = &kallsyms_names[ kallsyms_markers[pos>>8] ];
//依次跳过(pos&0xFF)个偏移即是当前符号的偏移地址处,(*name) + 1存的是len
for(i = 0; i < (pos&0xFF); i++)
name = name + (*name) + 1;//
return name - kallsyms_names;//返回该符号在kallsyms_names组中偏移
}
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];//取该sym的首地址
len = *data;//取sym压缩后的长度
data++;//指向压缩串
//指向下一个压缩串偏移
off += len + 1;
//为了解析而设置了数据结构kallsyms_token_table和kallsyms_token_indexkallsyms_token_table记录每个ascii字符的替代串,kallsyms_token_index记录每个ascii字符的替代串在kallsyms_token_table中的偏移.
while(len) {
//对于*data指向的字符,在token_index查找该字符所代表的解压串偏移,并从token_table中找到该解压串
tptr = &kallsyms_token_table[ kallsyms_token_index[*data] ];
data++;
len--;
while (*tptr) {
if(skipped_first) {//跳过类型字符,例如t,T
*result = *tptr;//拷贝解压串
result++;
} else
skipped_first = 1;
tptr++;
}
}
*result = '\0';
//返回下一个压缩串偏移
return off;
}
const char *module_address_lookup(unsigned long addr,unsigned long *size,unsigned long *offset,char **modname,char *namebuf)
{
struct module *mod;
const char *ret = NULL;
preempt_disable();
//遍历内核中的所有模块
list_for_each_entry_rcu(mod, &modules, list) {
if (mod->state == MODULE_STATE_UNFORMED)
continue;
//addr是否在模块的init部分或者core部分
if (within_module_init(addr, mod) ||within_module_core(addr, mod)) {
if (modname)
*modname = mod->name;//取得模块名
ret = get_ksymbol(mod, addr, size, offset);
break;
}
}
/* Make a copy in here where it's safe */
if (ret) {
strncpy(namebuf, ret, KSYM_NAME_LEN - 1);
ret = namebuf;
}
preempt_enable();
return ret;
}
static const char *get_ksymbol(struct module *mod,unsigned long addr,unsigned long *size,unsigned long *offset)
{
unsigned int i, best = 0;
unsigned long nextval;
/* At worse, next value is at end of module */
if (within_module_init(addr, mod))
nextval = (unsigned long)mod->module_init+mod->init_text_size;
else
nextval = (unsigned long)mod->module_core+mod->core_text_size;
/* Scan for closest preceding symbol, and next symbol. (ELF
starts real symbols at 1). */
//遍历模块的符号
for (i = 1; i < mod->num_symtab; i++) {
if (mod->symtab[i].st_shndx == SHN_UNDEF)//跳过未定义的符号
continue;
/* We ignore unnamed symbols: they're uninformative
* and inserted at a whim. */
if (mod->symtab[i].st_value <= addr
&& mod->symtab[i].st_value > mod->symtab[best].st_value
&& *(mod->strtab + mod->symtab[i].st_name) != '\0'
&& !is_arm_mapping_symbol(mod->strtab + mod->symtab[i].st_name))
best = i;
if (mod->symtab[i].st_value > addr
&& mod->symtab[i].st_value < nextval
&& *(mod->strtab + mod->symtab[i].st_name) != '\0'
&& !is_arm_mapping_symbol(mod->strtab + mod->symtab[i].st_name))
nextval = mod->symtab[i].st_value;
}
if (!best)
return NULL;
if (size)
*size = nextval - mod->symtab[best].st_value;
if (offset)
*offset = addr - mod->symtab[best].st_value;
return mod->strtab + mod->symtab[best].st_name;
}
六、符号属性
若符号在内核中是全局性的,则属性为大写字母,如T、U等。
b:符号在未初始化数据区(BSS)
c:普通符号,是未初始化区域
d:符号在初始化数据区
g:符号针对小object,在初始化数据区
i:非直接引用其他符号的符号
n:调试符号
r:符号在只读数据区
s:符号针对小object,在未初始化数据区
t:符号在代码段
u:符号未定义
linux内核kallsyms机制分析
精选 转载
提问和评论都可以,用心的回复会被更多人看到
评论
发布评论
相关文章
-
linux内核调试技巧三:kallsyms
linux内核调试技巧三:kallsyms
符号表 linux 内核配置 Linux -
linux内核机制常识
linux内核常识
linux内核常识 -
Linux内核——分段机制
2020.08.17 第二章
寄存器 可执行文件 数据段 -
Linux 内核DMA 机制
Linux 内核DMA 机制
Linux 职场 内核 休闲 DMA