学习原因
工作中,使用同事开发的调试软件,输入参数打印的函数名就可以打印参数,但看不到代码实现,只能用自己微薄的知识积累去猜一下,之前尝试过,专门写一个函数,去解析编译生成的map文件,就可以通过函数名去找到函数的地址,然后用函数指针去运行就可以了,最近工作之余在学习RT-Thread的内核源码,刚好看到了Finsh组件,可以在命令行输入函数名运行,所以就简单学习了下;
自定义MSH命令
举例引入
如下是最简单的“hello world"函数, 在函数定义下面有一行宏,纸面意思是Finsh函数导出,下面对宏进行分析:
void hello(void)
{printf("hello RT-Thread!\n");
}
FINSH_FUNCTION_EXPORT(hello, say hello world);
宏定义说明
先看下RT-Thread官方文档对如下宏定义的说明:
FINSH_FUNCTION_EXPORT(hello, say hello world);
宏定义源码分析
#define FINSH_FUNCTION_EXPORT(name, desc) \FINSH_FUNCTION_EXPORT_CMD(name, name, desc)/* else分支下的宏定义 */
#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \const char __fsym_##cmd##_name[] SECTION(".rodata.name") = #cmd; \const char __fsym_##cmd##_desc[] SECTION(".rodata.name") = #desc; \RT_USED const struct finsh_syscall __fsym_##cmd SECTION("FSymTab")= \{ \__fsym_##cmd##_name, \__fsym_##cmd##_desc, \(syscall_func)&name \};/* 上面宏定义中调用的宏的定义 */
#define SECTION(x) __attribute__((section(x)))
#define RT_USED __attribute__((used))/* 上面宏定义中使用的结构体定义 */
struct finsh_syscall
{const char* name; /* the name of system call */const char* desc; /* description of system call */syscall_func func; /* the function address of system call */
};/* 上面宏定义中使用的函数指针定义 */
typedef long (*syscall_func)(void);
宏展开后的真相
可以看到展开之后,是一个finsh_syscall结构体类型的变量"__fsym_hello",在后续章节可以得到证实;
FINSH_FUNCTION_EXPORT(hello, say hello world);/* 展开之后 */
const char __fsym_hello_name[] __attribute__((section(".rodata.name"))) = "hello"; \
const char __fsym_hello_desc[] __attribute__((section(".rodata.name"))) = "say hello world"; \
__attribute__((used)) const struct finsh_syscall __fsym_hello __attribute__((section("FSymTab"))) = \
{ \__fsym_hello_name, \__fsym_hello_desc, \(syscall_func)&hello \
};
代码编译
代码编译是在Ubuntu系统下,如下主要说明链接脚本文件生成和代码编译
链接文件生成
指令如下:
ld --verbose
生成信息如下,红框中的内容需要删除掉,最后一行红框中的内容也需要删除,否则编译出错
代码编译
代码编译是在Ubuntu系统下编译,编译指令如下:
gcc main.c -T link.lds -o main -Wl,-Map,test.map
其中"link.lds"链接时指定的链接文件,在上一个小节中生成,"test.map"是编译生成的map文件;
MAP文件分析
分析编译生成的map文件,如下是”__fsym_hello_name“和”__fsym_hello_desc“的段属性:
如下是"__fsym_hello"的段属性:
通过如上的分析,说明前面的宏定义分析是合理的,展开后的变量在编译生成的map文件中存在;
链接文件分析
本章节第一小节说明了链接文件的生成过程,”.rodata“段是链接脚本生成的时候就有的,需要在".text"段添加"FSymTab"段,需要添加的代码如下:
. = ALIGN(8);__fsymtab_start = .;KEEP(*(FSymTab))__fsymtab_end = .;
. = ALIGN(8);
添加完成后的".text" 段如下:
主要关注下:"__fsymtab_start"和"__fsymtab_end",是"FSymTab"段的首地址变量和尾地址变量,可以在代码中使用;如果不添加如上代码段,在代码中就无法使用这2个变量,同时也无法编译通过;
代码实现分析
在板级代码中分析,在初始化任务中会调用finsh_system_init()函数,其注释是:初始化Finsh,所以就从这个代码入手分析,摘取了主要代码如下:
初始化关键变量
void finsh_system_init(void)
{extern const int __fsymtab_start;extern const int __fsymtab_end;finsh_system_function_init(&__fsymtab_start, &__fsymtab_end);
}/* 全局变量声明 */
struct finsh_syscall *_syscall_table_begin = NULL;
struct finsh_syscall *_syscall_table_end = NULL;/* 函数调用 */
void finsh_system_function_init(const void *begin, const void *end)
{_syscall_table_begin = (struct finsh_syscall *) begin;_syscall_table_end = (struct finsh_syscall *) end;
}
帮助信息函数
如上代码主要是设置了"_syscall_table_begin"和“_syscall_table_end”这2个全局变量,然后根据这2个全局变量找下是咋用的,找到了函数msh_help(),这个函数也导出了,在命令行输入也可以运行,并输出help帮助信息;
nt msh_help(int argc, char **argv)
{rt_kprintf("RT-Thread shell commands:\n");{struct finsh_syscall *index;for (index = _syscall_table_begin;index < _syscall_table_end;FINSH_NEXT_SYSCALL(index)){if (strncmp(index->name, "__cmd_", 6) != 0) continue;
#if defined(FINSH_USING_DESCRIPTION) && defined(FINSH_USING_SYMTAB)rt_kprintf("%-16s - %s\n", &index->name[6], index->desc);
#elsert_kprintf("%s ", &index->name[6]);
#endif}}rt_kprintf("\n");return 0;
}
FINSH_FUNCTION_EXPORT_ALIAS(msh_help, __cmd_help, RT-Thread shell help.);/* for循环里面用到的宏定义 */
#define FINSH_NEXT_SYSCALL(index) index=finsh_syscall_next(index)/* 宏定义里面调用的函数 */
struct finsh_syscall* finsh_syscall_next(struct finsh_syscall* call)
{unsigned int *ptr;ptr = (unsigned int*) (call + 1);while ((*ptr == 0) && ((unsigned int*)ptr < (unsigned int*) _syscall_table_end))ptr ++;return (struct finsh_syscall*)ptr;
}
MSH命令执行过程
在finsh_system_init()函数里面创建了个任务,猜想分析应该是用来执行shell指令的,相关的代码如下,需要说明下,如下代码就大概看了下,可能有问题,请勿完全相信:
int finsh_system_init(void)
{rt_thread_t tid = &finsh_thread;result = rt_thread_init(&finsh_thread,FINSH_THREAD_NAME,finsh_thread_entry, RT_NULL,&finsh_thread_stack[0], sizeof(finsh_thread_stack),FINSH_THREAD_PRIORITY, 10);rt_thread_startup(tid);
}void finsh_thread_entry(void *parameter)
{while (1){ch = finsh_getchar();msh_exec(shell->line, shell->line_position);}}int msh_exec(char *cmd, rt_size_t length)
{if (_msh_exec_cmd(cmd, length, &cmd_ret) == 0){return cmd_ret;}
}static int _msh_exec_cmd(char *cmd, rt_size_t length, int *retp)
{cmd_function_t cmd_func;cmd_func = msh_get_cmd(cmd, cmd0_size);*retp = cmd_func(argc, argv);
}
简单代码实现
MSH部分的源代码
根据以上的分析,写了个简单的测试程序如下:
void finsh_system_init(void)
{extern const int __fsymtab_start;extern const int __fsymtab_end;unsigned int addr_offset = (char *)&__fsymtab_start + 8;//_syscall_table_begin = (struct finsh_syscall *) &__fsymtab_start;/* 看map表偏移了8byte,指向了FSymTab段第一个变量的首地址 */_syscall_table_begin = (struct finsh_syscall *) addr_offset; _syscall_table_end = (struct finsh_syscall *) &__fsymtab_end;printf("addr_offset = %x\n",addr_offset);printf("&__fsymtab_start = %x\n",&__fsymtab_start);printf("&__fsymtab_end = %x\n",&__fsymtab_end);printf("_syscall_table_begin = %x\n",_syscall_table_begin);printf("_syscall_table_end = %x\n",_syscall_table_end);printf("syscall_table length = %ld\n\n\n",((unsigned int)&__fsymtab_end) - ((unsigned int)&__fsymtab_start));
}void msh_help()
{struct finsh_syscall *index;for (index = _syscall_table_begin;index < _syscall_table_end; FINSH_NEXT_SYSCALL(index)){printf("%-8s \t - %s\n", &index->name[0], index->desc); index->func();printf("\n\n");}
}
代码分析
在finsh_system_init()函数中,_syscall_table_begin指向的是"FSymTab"段第一个变量的首地址,而源代码中指向的是"FSymTab"段的首地址变量,这块应该是和地址对齐有关,因时间等原因没仔细分析,查看map文件后,简单的把地址做了个便宜;
在源码的msh_help()函数中,遍历了"FSymTab"段中的各变量,打印函数名及函数描述信息,所以我增加了一行index->func(),这一行就可以调用导出的函数;
MAP文件分析
如下是编译生成的map文件中,"FSymTab"段的地址信息,其中段的首地址变量和尾地址变量的地址用红框标注,首尾地址之间的是2个段变量,其实就是前面宏展开后的变量;可以看到段首地址变量的地址和绿框中第一个段变量的地址差了8,所以在代码里面强制偏移了8;同时注意到,绿框中2个变量的首地址相差32byte,因为”finsh_syscall“结构体的大小是32byte;
要导出的函数
上一小节的map文件中,有2个变量,这2个变量就是测试代码中导出的2个函数:
void rt_show_version(void)
{printf("\n \\ | /\n");printf("- RT - Thread Operating System\n");printf(" / | \\ %d.%d.%d build %s\n",3, 0, 05, __DATE__);printf(" 2006 - 2018 Copyright by rt-thread team\n");
}
FINSH_FUNCTION_EXPORT(rt_show_version, show rt_thread version);void hello(void)
{printf("hello RT-Thread!\n");
}
FINSH_FUNCTION_EXPORT(hello, say hello world);
同时在主函数里面只需要调用finsh_system_init()和 msh_help()即可
运行结果
代码运行结果如下,”finsh_syscall“结构体的大小是32byte;导出的函数rt_show_version()和hello()也正常运行了;地址也和map文件完全一致;