在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
1 自定义段
1.1 定义段
gcc支持自定义段。格式如下:
/* my_section为自定义段名 */
__attribute__((section(my_section)))
使用自定义段修饰的变量或函数在编译链接时会被放入指定的段中。
例如:
/* global_var会被放到my_section段 */
__attribute__((section(my_section))) int global_var = 1;
/* fun函数会被放到my_section段 */
__attribute__((section(my_section))) int fun(void){}
Tips:
- 本文中介绍的是基于Linux系统和gcc编译器自带的lds新增自定义段。
- 基于第一点,对于自定义的段名是有限制的——不允许以”."开头,因为以“."开头的段都是系统保留的,例如.txt和.data等。
- 使用自定义段时请不要再自己定义__GNUC__宏,否则自定义段不生效。
1.2 引用段
# 使用readelf命令查看符号表(option是小s)
readelf -s help_elf
结果如下:
54: 00000000006010fc 0 NOTYPE GLOBAL DEFAULT 26 __stop_help_cmd
67: 0000000000601034 0 NOTYPE GLOBAL DEFAULT 26 __start_help_cmd
我的代码中自定义的段名是help_cmd,编译器会自动生成该段起始地址变量(起始地址变量这个名字很奇怪,后面解释)和结束地址变量。
需要注意的是 &__start_help_cmd 才是段help_cmd的起始地址,而 __start_help_cmd只是一个变量,变量类型在extern时指定。如下例:
/* 声明编译器自动生成的段起始地址对应的变量,其中cmd_tbl_t是一个结构体类型 */
extern cmd_tbl_t __start_help_cmd;
/* 注意下面这种声明是错误的(因为存放时存放的是cmd_tbl_t类型的变量,不是指针) */
extern cmd_tbl_t *__start_help_cmd;
另外,还需要说明的一点是,&__stop_help_cmd对应的地址是自定义段help_cmd结束后的下一个字节对应的地址(和对齐无关)。
2 段的对齐属性
在调试该功能过程中遇到段错误问题,后来定位发现是段对齐属性导致。所以,自定义段时一定要同步设置段的地址对齐属性,防止出现段错误问题,或者更换编译器、系统之后出现问题。
下面,通过一个例子来说明段对齐属性的重要性。
2.1 默认对齐
我一开始使用的是“标准”的自定义段的方式定义的变量,代码如下:
/* 自定义段:help_cmd */
#define Struct_Section __attribute__ ((unused,section ("help_cmd")))
#define HELP_ADD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __help_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
/* 定义两个变量到自定义段 */
HELP_ADD(dabaoxin,1,1,2,"I like playing basketball","help");
HELP_ADD(dabaoge,1,1,2,"I like eatting ","help");
其中,cmd_tbl_t为我自定义的结构体类型(在下一节中会展开介绍)。当检索第一个指令dabaoxin时可以正确访问该结构体内的成员。问题就出在访问第二个指令dabaoge所在结构体时。因为,使用指针自加操作时,找到的并不是第二个结构体的基地址,从而造成段错误。到这里,没明白我说什么不重要,请继续往下看~ 真相会逐渐浮出水面~
为了更清楚的说明这个问题,我们来看下编译出的elf文件的内部结构(基础知识请参考《程序员的自我修养》这本书)。
首先,我们使用下述指令看一下段表内的信息。我们发现help_cmd段的Align参数默认居然是32个字节。也就是说在我们自定义的段help_cmd中每个成员的首地址必须是32的整数倍。
# 读取elf段表信息(option是大S)
readelf -S help_elf
! 默认对齐属性的段表信息
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[25] .data PROGBITS 0000000000601030 00001030
0000000000000004 0000000000000000 WA 0 0 1
[26] help_cmd PROGBITS 0000000000601040 00001040
0000000000000128 0000000000000000 WA 0 0 32
[27] .bss NOBITS 0000000000601168 00001168
0000000000000008 0000000000000000 WA 0 0 1
当时,在定位问题时,为了更清楚的看到自定义段help_cmd的地址和内容,使用了反汇编指令,指令如下。
我们可以看到,在段help_cmd中存放一个cmd_tbl_t结构的变量居然占用了64个字节,而一个cmd_tbl_t的真正大小只有40个字节(通过sizeof关键字或者readelf -s(小s)指令都可以确认这一点)。
所以,如果我们定义一个cmd_tbl_t类型的指针p指向 &__start_help_cmd的话,p++指向的地址是0x601068,并不是我们期望的0x601080。而0x601068的内容全部是空,如果结构体内的成员是指针类型的话必然会产生空指针解引用,从而导致段错误。
那如何解决这个问题呢?
答案是在自定义段的同时声明地址对齐方式。
# 获取反汇编完整信息(option是小s)
objdump -s help_elf
! 32字节对齐反汇编结果
Contents of section help_cmd:
601040 10074000 00000000 01000000 01000000 ..@.............
601050 02000000 00000000 19074000 00000000 ..........@.....
601060 33074000 00000000 00000000 00000000 3.@.............
601070 00000000 00000000 00000000 00000000 ................
601080 38074000 00000000 01000000 01000000 8.@.............
601090 02000000 00000000 40074000 00000000 ........@.@.....
6010a0 33074000 00000000 00000000 00000000 3.@.............
6010b0 00000000 00000000 00000000 00000000 ................
6010c0 50074000 00000000 01000000 01000000 P.@.............
6010d0 02000000 00000000 40074000 00000000 ........@.@.....
6010e0 33074000 00000000 3.@.....
2.2 指定对齐
在自定义段的同时指定对齐方式的格式如下:
/* 自定义段的同时指定段内元素地址对齐方式为:以4字节对齐 */
__attribute__ ((unused,section ("help_cmd"), aligned(4)))
再来看看指定段help_cmd以4字节对齐之后的段表信息和反汇编结果。我们发现,段表信息中help_cmd的Align属性已经变成了4字节。
在反汇编结果中我们也发现,元素间是 “紧凑”存放的了,这样先前定义的 p指针在进行自加操作后便可以精准指向第二个结构体变量元素了。段错误问题完美解决~~
! 修改对齐属性后的段表信息
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[25] .data PROGBITS 0000000000601030 00001030
0000000000000004 0000000000000000 WA 0 0 1
[26] help_cmd PROGBITS 0000000000601034 00001034
00000000000000c8 0000000000000000 WA 0 0 4
[27] .bss NOBITS 00000000006010fc 000010fc
0000000000000004 0000000000000000 WA 0 0 1
! 修改对齐属性后的反汇编结果
Contents of section help_cmd:
601034 20074000 00000000 01000000 01000000 .@.............
601044 02000000 00000000 29074000 00000000 ........).@.....
601054 43074000 00000000 48074000 00000000 C.@.....H.@.....
601064 01000000 01000000 02000000 00000000 ................
601074 50074000 00000000 43074000 00000000 P.@.....C.@.....
601084 60074000 00000000 01000000 01000000 `.@.............
601094 02000000 00000000 50074000 00000000 ........P.@.....
6010a4 43074000 00000000
3 指令注册
我们最终要达到的效果是这个样子:添加一个帮助指令,在填写函数头的过程中就完成注册。
函数头的格式如下:
/* HELP_ADD 使用举例 */
HELP_ADD(
fun, /*function name*/
"测试函数", /*brief*/
3, /*the number of parameters*/
"u32 u32i:通道号(取值范围0-15)", /*parameters*/
"u8 u8j :栅压点",
"u32 u32k: 电压值(单位mV)");
下面让我们剖析实现细节。
来不及解释了,先上代码。详细解释请参照代码注释和后续叨叨~
/* help.h 文件内容 */
#ifndef __HELP_H__
#define __HELP_H__
/* 为了提高搜索速度定义的编译器优化选项(借鉴Linux)*/
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
/* 基于位段做的“宏定义在编译阶段的入参检查方法” */
#define SUPPORT_MAX_ARGS 5
#define CHECK(name,x) struct __check##name{unsigned:(-!!(x));};
/* help指令数据结构定义 */
#pragma pack(4)
typedef struct {
char *name; /* Command Name */
char *brief; /* Usage brief message (short) */
int argc; /* maximum number of arguments */
char *argv[SUPPORT_MAX_ARGS]; /* Help message of each arguments (long) */
}cmd_tbl_t;
#pragma pack()
/* 自定义段 */
#define Struct_Section __attribute__ ((unused,section ("help_cmd"), aligned(4)))
/* help指令添加宏方法定义 */
#define HELP_ADD(name,brief,argc,args...) \ /*args使用可变参数列表*/
CHECK(name, argc>SUPPORT_MAX_ARGS); \ /*在编译阶段检查入参,防止指令检索时越界访问造成任务异常*/
cmd_tbl_t __help_cmd_##name Struct_Section = {#name, brief, argc, {args}}
4 指令检索
4.1 help搜索
help搜索想达到的效果是给定记忆中的关键词(全部或者部分),然后直接跟在help后面就可以将相关指令格式化输出。
搜索实现的关键代码如下:
下面是help指令的实现,主要是在某些情况下调用help_usage函数来提示使用方法;调用find_cmd函数来查找用户输入的关键字,并将关键字匹配的所有指令整齐地打印出来。
/* help搜索实现 */
extern cmd_tbl_t __start_help_cmd;
extern cmd_tbl_t __stop_help_cmd;
int help(char *argv)
{
int i = 0;
cmd_tbl_t *pstr = &__start_help_cmd;
char *format_str = NULL;
if (NULL == argv){
return help_usage();
}
format_str = argv;
printf("[debug] format_str = %s .\n", format_str);
/* print usage */
if ('?' == *format_str){
return help_usage();
}
/* find cmd */
do{
pstr = find_cmd(format_str, pstr);
if (NULL != pstr){
printf("----------------------------------------------------------------------------\n");
printf("|| name \t| %s \n",pstr->name);
printf("|| brief \t| %s \n",pstr->brief);
for (i = 0; i < pstr->argc; i++){
if (pstr->argv[i])
printf("|| @ para%d\t| @ %s \n", i, pstr->argv[i]);
}
printf("----------------------------------------------------------------------------\n");
pstr++;
}
}while(pstr);
return 0;
}
下面是搜索相关的代码实现,为了较好地提升搜索的速度和简便地实现大小写忽略,没有直接使用strstr库函数,而使用了sunday搜索算法。代码如下:
find_cmd函数实现,主要功能是在自定义段中搜索与关键字匹配的指令结构体并返回。当前只支持基于命令名称的英文搜索,该函数可扩展成支持基于命令brief的中文搜索(如果你的指令说明是中文的话可能用起来更亲民一些)。
cmd_tbl_t *find_cmd (const char *cmd, cmd_tbl_t *cmd_start_p)
{
cmd_tbl_t *cmdtp = cmd_start_p;
int offset = -1;
for (;cmdtp != &__stop_help_cmd; cmdtp++){
if ((offset = sunday_searchi(cmd, cmdtp->name)) >= 0){
return cmdtp;
}
}
return NULL; /* not found */
}
下面是sunday算法的实现,据说大部分文本的搜索都是使用的该种搜索算法,既简单又高效。
static inline int sunday_searchi(const char * const format, const char * const raw)
{
int format_len = 0;
int raw_len = 0;
int offset = -1;
int i = 0;
const char *pr = raw;
if (unlikely(NULL == format || NULL == raw)){
printf("%s: NULL pointer fault!\n", __FUNCTION__);
return -1;
}
format_len = strlen(format);
raw_len = strlen(raw);
if (unlikely(format_len > raw_len)){
return -1;
}
while(pr -raw + format_len <= raw_len){
/* compare(ignore case) */
for (i = 0; i < format_len; i++){
if ((format[i]|0x20) != (pr[i]|0x20)) /*忽略英文大小写*/
break;
if (format_len-1 == i)
return pr - raw;
}
/* move */ /*该算法的核心就在于此处的指针移动*/
offset = find_last_chari(*(pr + format_len), format);
pr += format_len - offset;
}
return -1;
}
static inline int find_last_chari(const char c, const char * const raw)
{
const char *prtail = raw + strlen(raw) -1;
if (unlikely(NULL == raw)){
printf("%s: NULL pointer fault!\n", __FUNCTION__);
return -1;
}
do{
if ((*prtail|0x20) == (c|0x20))
return prtail - raw;
prtail--;
}while (prtail >= raw);
return -1;
}
4.2 help ?
为了更加人性化,可以在用户只输入help或者 help ?时给出一定提示信息,比如使用方法或者一些常用关键字。
int help_usage(void)
{
int i = 0;
printf("\n--------------------------------------------------\n");
printf("|| 使用方法\t\t| rr + 关键字(关键字可简写) \n");
printf("--------------------------------------------------\n");
printf("--------------------------------------------------\n");
printf("|| 关键字\t\t| 含义 \n");
printf("--------------------------------------------------\n");
for (; i < sizeof(stHelpUsage)/sizeof(help_usage_t); i++)
{
printf("|| %-10s\t| %s 相关指令 \n", stHelpUsage[i].englishName, stHelpUsage[i].chineseName);
printf("--------------------------------------------------\n");
}
return 0;
}
关键字结构体定义
typedef struct{
char *englishName;
char *chineseName;
}help_usage_t;
help_usage_t stHelpUsage[] = {
{"ver", "版本查询"}
/* add here in module */
};
<完>