基于自定义段技术构建指令系统

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

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:

  1. 本文中介绍的是基于Linux系统和gcc编译器自带的lds新增自定义段。
  2. 基于第一点,对于自定义的段名是有限制的——不允许以”."开头,因为以“."开头的段都是系统保留的,例如.txt和.data等。
  3. 使用自定义段时请不要再自己定义__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 */
};

<完>

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值