- 一、前言
- 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 image,相当于内核中存了一个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数组的偏移处取得字符串,字符串以“ ”隔开
- 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 = ' ';
- //返回下一个压缩串偏移
- 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) != ' '
- && !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) != ' '
- && !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:符号未定义