小安派教程之手把手分析shell代码,教你写自己的shell命令

[复制链接]
查看924 | 回复7 | 2023-12-19 16:26:19 | 显示全部楼层 |阅读模式

本帖最后由 lovzx 于 2023-12-21 11:17 编辑

本帖最后由 lovzx 于 2023-12-20 11:53 编辑

本帖最后由 lovzx 于 2023-12-19 20:51 编辑

手把手分析shell代码,教你写自己的shell命令

本文内容涉及到

  • 分析shell命令有哪些,是如何被识别成shell的,涉及到一点点asm汇编文件(不需要会asm,本人也是边看边学边写教程的,遇到不会的面向搜索编程就可以一箭破万法)
  • 分析shell命令的执行流程
  • 实现一个简单的kill命令,该命令是关闭指定的task任务

教程基于小安派S1(bl616dk)开发板和小安派开发环境,文中设计到的所有代码放在在结尾开源地址中,有需自取,欢迎评论交流,有写的不好的地方或者理解错误,或者有不同见解,都可以评论交流!!!

配置SHELL程序

proj.conf配置

  • shell 组件必须要开启
set(CONFIG_SHELL 1)

{SDK}/components/shell中的shell_freertos.c中有用到freertos的task和信号量,所以还必须开启freertos配置

set(CONFIG_FREERTOS 1)

其他的组件不是必须的

proj.conf代码如下

set(CONFIG_SHELL 1)
  • {SDK}/components/shell中的shell_freertos.c中有用到freertos的task和信号量,所以还必须开启freertos配置
set(CONFIG_FREERTOS 1)

其他的组件不是必须的

proj.conf代码如下

//shell 程序必须依赖组件
set(CONFIG_SHELL 1)
set(CONFIG_FREERTOS 1)


//非必须,下面都是其他shell命令有需要用到的
set(CONFIG_POSIX 1)
set(CONFIG_TLSF 1)
set(CONFIG_LWIP 1)
set(CONFIG_WIFI6 1)
set(CONFIG_RF 1)
set(CONFIG_MBEDTLS 1)
set(CONFIG_DHCPD 1)
set(CONFIG_PING 1)

.h配置文件

  • 复制其他demo中的FreeRTOSConfig.h文件(freertos必须的配置文件)
  • 复制其他demo中的lwipopts_user.h文件
  • 复制其他demo中的mbedtls_sample_config.h文件

shell 初始化

示例中,shell交互是通过uart0串口交互的,拿到uart0设备后通过shell_init_with_task初始化shell

核心是注册uart0设备中断,当uart0设备有数据是就会触发中断函数,从而拿到uart串口数据,解析shell命令

int main(void)
{
    struct bflb_device_s* uart0;
    //获取uart0串口
    uart0 = bflb_device_get_by_name("uart0");
    //初始化shell
    shell_init_with_task(uart0);
}

void shell_init_with_task(struct bflb_device_s *shell)
{
    uart_shell = shell;
    //创建二值信号量
    vSemaphoreCreateBinary(sem_shell);
    //设置uart rx状态
    bflb_uart_rxint_mask(uart_shell, false);
    //把uart_shell_isr函数注册到uart0串口设备中断中
    bflb_irq_attach(uart_shell->irq_num, uart_shell_isr, NULL);
    //启动中断
    bflb_irq_enable(uart_shell->irq_num);

    //设置shell read buffer
    Ring_Buffer_Init(&shell_rb, shell_buffer, sizeof(shell_buffer), NULL, NULL);

    //初始化shell
    shell_init();
    //创建shell任务
    xTaskCreate(shell_task, (char *)"shell_task", SHELL_THREAD_STACK_SIZE, NULL, SHELL_THREAD_PRIO, &shell_handle);
}

shell命令注册

shell命令的注册是在shell_init函数中完成的,通过shell_funcation_init函数将shell_syscall结构体保存在FSymTab section中, 并赋值给_sys_call_beign和_sys_call_end

利用section构建数据表

代码如下:

void shell_init(void)
{

    //初始化shell命令
    shell_function_init(__section_begin("FSymTab"), __section_end("FSymTab"));
    //初始化shell参数
    shell_var_init(__section_begin("VSymTab"), __section_end("VSymTab"));

    shell = &_shell;
    //设置shell name
    shell_set_prompt(SHELL_DEFAULT_NAME);
    //设置shell输出函数
    shell_set_print((void (*)(char *fmt, ...))printf);
}

ASM文件分析

关于section的作用

section作用

从上文中摘录出来,section可以通过编译器内建宏__section_begin和__section_end来定位到代码地址,返回值都是void*类型

项目编译成功后打开build/build_out/shell_demo_bl616.asm文件,搜索shell_init(),找到如下代码

//调用Ring_Buffer_Init函数
a002cb78:   280d                    jal a002cbaa <Ring_Buffer_Init>
    shell_init();//调用shell_init函数
a002cb7a:   85cff0ef            jal ra,a002bbd6 <shell_init>

ps:生成的asm文件会有源码和asm汇编代码,可以对照着看

搜索 RISC jal指令可以知道jal是跳转指令

后面的ra是寄存器。保存了下一条指令的地址,a002bbd6则是真正要跳转的地址

知道要跳转的地址后继续在asm文件中搜索a002bbd6:(加:是为了快速匹配到地址,而不是其他文本数据等)

搜索到的结果如下

static void shell_function_init(const void *begin, const void *end)
{
    _syscall_table_begin = (struct shell_syscall *)begin;
a002bbd6:   a00a37b7            lui a5,0xa00a3
a002bbda:   62fcb737            lui a4,0x62fcb
a002bbde:   00478793            addi    a5,a5,4 # a00a3004 <__psram_limit+0xf7ca3004>
a002bbe2:   74f72023            sw  a5,1856(a4) # 62fcb740 <_syscall_table_begin>
    _syscall_table_end = (struct shell_syscall *)end;
a002bbe6:   a00a37b7            lui a5,0xa00a3
a002bbea:   62fcb737            lui a4,0x62fcb
a002bbee:   10478793            addi    a5,a5,260 # a00a3104 <__psram_limit+0xf7ca3104>
a002bbf2:   74f72223            sw  a5,1860(a4) # 62fcb744 <_syscall_table_end>
}

可以看到搜索到的直接进入了shell_function_init函数,

通过查找lui指令和addi指令可以得出最终a5寄存器的值是a00a3004也就是#号后面的注释

然后赋值给了_syscall_table_begin变量,通过注释可以看出来这个变量是shell_syscall结构体类型。

C语言中结构体决定了如何去解析一个地址里面的数据,以及该数据的大小,通过代码shell.h中可以找到shell_syscall结构体如下

struct shell_syscall {
    const char *name; /* the name of system call */
#if defined(SHELL_USING_DESCRIPTION)
    const char *desc; /* description of system call */
#endif
    syscall_func func; /* the function address of system call */
};

其中desc由于宏定义没有打开可以直接忽略

前面我们已经知道了shell_function_init函数中最终会把变量地址a00a3004复制给_syscall_table_begin的对象,而这个对象是shell_syscall结构体的指针,接下来我们重新回到asm文件中搜索**a00a3004:**就可以看到下面代码

a00a3004 <__fsym___cmd_free>:
a00a3004:   5a50 a00a 1a1e a001                         PZ......

a00a300c <__fsym___cmd_ping>:
a00a300c:   9b10 a00a 6b9c a002                         .....k..

a00a3014 <__fsym___cmd_memtrace>:
a00a3014:   a8a0 a00a b924 a002                         ....$...

a00a301c <__fsym___cmd_help>:
a00a301c:   a894 a00a b838 a002                         ....8...

观察不难发现asm代码结构上分为3列,最左边的2个字节十六进制数据和:代表的是地址,中间<__fsym__cmd_free>:这段字节码对应的源码,最右边表示的是字节码直接按照asicc翻译的内容,由于字节码数据并不都是asicc内容,所以右边的只能当作参考,只有明确数据内容是asicc是右边的转换才有效,其他类型的数据没有什么参考价值

asm中显示的数据和shell_syscall的关系

从前面的分析已经知道了,**_syscall_table_begin = (struct shell_syscall *)a00a3004**,a00a3004这个地址中存放的数据正是shell_syscall结构体数据,上面分析以及知道了shell_syscall结构体中只包含了name和func两个字段,分别对应的是char *和一个函数指针,可以通过简单的sizeof运算符得到小安派编译器中指针占用4个字节,而shell_syscall的大小刚好是8个字节,也就是说,前面4个字节对应的是char字符串的首地址,后四个字节对应的是函数的地址(也就是函数指针)。单独拿出下面一条数据来观察

a00a3004 <__fsym___cmd_free>:
a00a3004:   5a50 a00a 1a1e a001

通过刚才的分析发现5a50 a00a 1a1e a001这8个字节都是代表的地址,巧合的是前面我们分析过a00a3004也是地址,这8个字节中都有a00a还都是放在高地址空间,所以地址正确的读法顺序(小端序)应该是a00a5a50和a00a1a1e这两个指针,a00a5a50存放的name的地址,而a0011a1e则是func的地址,

搜索asm中的这两个地址,找到代码可以看到name的内容为__cmd_free,也就是5f5f 6d63 5f64 7266 6565这10个字节对应的数据6565后面00则标志着字符串的结束,而func函数则是cmd_free

a00a5a50:   5f5f 6d63 5f64 7266 6565 0000 7375 6465     __cmd_free..used

int cmd_free(int argc, char **argv)
{
a0011a1e:   7179                    addi    sp,sp,-48
a0011a20:   d422                    sw  s0,40(sp)

}

shell_syscall的注册

通过搜索可以发现SDK目录中mem.c中定义了改函数,打开mem.c不难发现文件末尾有引用cmd_free的宏定义,

C语言attribute用法

代码如下

SHELL_CMD_EXPORT_ALIAS(cmd_free, free, show memory usage);

按住ctrl然后鼠标左键点击宏定义就会跳转到shell.h中,相关的宏定义如下

#define SHELL_CMD_EXPORT_ALIAS(command, alias, desc) \
    SHELL_FUNCTION_EXPORT_CMD(command, __cmd_##alias, desc)

#define SHELL_FUNCTION_EXPORT_CMD(name, cmd, desc)                                                        \
    const char __fsym_##cmd##_name[] = #cmd;                                                              \
    __attribute__((used)) const struct shell_syscall __fsym_##cmd __attribute__((section("FSymTab"))) = { \
        __fsym_##cmd##_name,                                                                              \
        (syscall_func)&name                                                                               \
    };

mem.c中注册shell的代码展开后就变成了下面这样

//展开1
SHELL_FUNCTION_EXPORT_CMD(cmd_free,__cmd_free, show memory usage);

//展开2
const char __fsym__cmd_free_name[] = "__cmd_free";
__attribute__((used)) const struct shell_syscall __fsym___cmd_free __attribute__((section("FSymTab"))) = {__fsym__cmd_free_name,(syscall_func)&cmd_free};

__attribute__((section("FSymTab")))的作用是将代码放入段"FSymTab"中,而不是默认的代码段中, 打开build/build_out/xxx.map文件找到下面代码

0x00000000a00a3004                __fsymtab_start = .
 *(FSymTab)
 FSymTab        0x00000000a00a3004        0x8 build_out\lib\libmm.a(mem.c.obj)
                0x00000000a00a3004                __fsym___cmd_free
 FSymTab        0x00000000a00a300c        0x8 build_out\lib\libping.a(ping.c.obj)
                0x00000000a00a300c                __fsym___cmd_ping
                ....
                0x00000000a00a30e4                __fsym___cmd_wifi_scan
                0x00000000a00a30ec                __fsym___cmd_phy
                0x00000000a00a30f4                __fsym___cmd_hello
 FSymTab        0x00000000a00a30fc        0x8 build_out\lib\libapp.a(board.c.obj)
                0x00000000a00a30fc                __fsym___cmd_reboot
                0x00000000a00a3104                __fsymtab_end = .

可以看到fsymtab段的开始地址地址和结束地址分别是a00a3004和a00a3104,前面分析到asm文件中__fsym__cmd_free的地址也是a00a3004,map文件中fsymtab段起始地址正是__fsym___cmd_free,同理也可以找到asm文件中fsymtab段的结尾地址a00a3104

在map文件中还可以找到每个shell命令是由那个源文件编译出来的

//段名称         指令地址                       编译出的库文件以及对应的源码文件
 FSymTab        0x00000000a00a300c        0x8 build_out\lib\libping.a(ping.c.obj)
                0x00000000a00a300c                __fsym___cmd_ping

根据上面注释可以知道ping命令是由ping.c文件注册编译出来的,该命令的地址是a00a300c,__fsym___cmd_ping是shell_syscall结构体name对应的字符串内容

所有的shell命令

从map中的fsymtab段可以找到所有的shell命令的起始地址,或者是在asm文件中找到fsymtab的起始地址,然后根据每8个字节就是一个shell_syscall结构体对象,前4个字节是shell_syscall结构体的名字,后4个字节是真正执行命令的代码入口地址(函数指针),根据前面的宏定义可以得到shell命令其实是结构体name对应的字符串去掉__fsym___cmd_后的内容,例如__fsym___cmd_ping结构体对应的shell命令就是ping

shell如何注册的

其实到这里就应该知道为什么我们明明没有写shell命令相关的代码就默认有32个shell命令,上面map文件中可以看到board.c中注册了reboot命令,mem.c注册了free命令,wifi_mgmr.c中注册了ap_和sta_等wifi相关的命令,像是board.c,mem.c等基本都是必须要依赖的,import了这些头文件,编译器编译对应的.c源文件时就自然的就注册了shell命令

小结

到这里已经弄明白了所有的shell命令都是由shell_syscall定义的,并且存放在FSymtab代码段中,其地址是a00a3004~a00a3104,一共256个字节,每个shell_syscall结构体占8字节,也就是有32个shell命令(我数了,SDK默认自带的确实是32个)

shell执行流程

前面分析了shell_syscall结构体及内存地址,也知道了初始化的时候注册了uart0设备的中断函数,当uart0设备有数据进入中断时,就会执行前面注册的uart_shell_isr中断函数,关键就在于uart_shell_isr中断函数中,先上代码

void uart_shell_isr(int irq, void *arg)
{
    //uart_shell 其实就是uart0设备,这里拿到的就是uart0设备的状态
    uint32_t intstatus = bflb_uart_get_intstatus(uart_shell);
    //如果设备是RX FIFO中断,把从uart0读取的数据写入到shell_rb缓存中
    if (intstatus & UART_INTSTS_RX_FIFO) {
        while (bflb_uart_rxavailable(uart_shell)) {
            Ring_Buffer_Write_Byte(&shell_rb, bflb_uart_getchar(uart_shell));
        }
        shell_release_sem();
    }
    //如果是读取超时中断,复制到缓存shell_rb中,并且重置中断标志,准备下次数据的接受
    if (intstatus & UART_INTSTS_RTO) {
        while (bflb_uart_rxavailable(uart_shell)) {
            Ring_Buffer_Write_Byte(&shell_rb, bflb_uart_getchar(uart_shell));
        }
        shell_release_sem();
        bflb_uart_int_clear(uart_shell, UART_INTCLR_RTO);
    }
}

uart中断这块可以看小泽老师的uart教程

shell_task读取数据

前面分析到shell_init_with_task的时候不仅初始化了shell_init函数,而且还创建了shell_task任务,该任务的作用就是不停的读取shell_rb缓存中的数据,并且调用执行shell_handler函数

static void shell_task(void *pvParameters)
{
    uint8_t data;
    uint32_t len;
    while (1) {
        if (xSemaphoreTake(sem_shell, portMAX_DELAY) == pdTRUE) {
            len = Ring_Buffer_Get_Length(&shell_rb);
            for (uint32_t i = 0; i < len; i++) {
                Ring_Buffer_Read_Byte(&shell_rb, &data);
                shell_handler(data);
            }
        }
    }
}

shell_handler中会根据接收到的数据内容做出不同的处理,下面是部分shell_handler代码,详情可以自行查看shell.c文件(代码中涉及到键盘键值对应表)

void shell_handler(uint8_t data)
{
    //ctrl + c,发送SHELL_SIGINT退出中断shell命令的执行
    if (data == 0x03) {
        /*!< ctrl + c */
        if (shell_sig_func) {
            //执行中断需要shell命令执行的时候调用shell_signal注册回调函数,否则是不会执行的
            shell_sig_func(SHELL_SIGINT);
            //执行完就设置为NULL了,所以需要每个shell自己手动处理shell_signal
            shell_sig_func = NULL;
        } 
        SHELL_PRINTF("^C");
        data = '\r';
    }
    // 发送\r或者\n才会执行shell命令,这也是后面需要设置串口助手发送末尾加上回车换行的原因
    /* handle end of line, break */
    if (data == '\r' || data == '\n') {
        //执行前把上一个shell命令放入到history中
        shell_push_history(shell);

        SHELL_PRINTF("\r\n");
        //开始执行shell命令
        shell_exec(shell->line, shell->line_position);

        SHELL_PROMPT(shell_get_prompt());
        memset(shell->line, 0, sizeof(shell->line));
        shell->line_curpos = shell->line_position = 0;
        return;
    }
    //不是其他特殊字符,复制data到shell->line数组中,当作shell输入来执行
        /* normal character */
    if (shell->line_curpos < shell->line_position) {
        int i;

        memmove(&shell->line[shell->line_curpos + 1],
                &shell->line[shell->line_curpos],
                shell->line_position - shell->line_curpos);
        shell->line[shell->line_curpos] = data;

        SHELL_PRINTF("%s", &shell->line[shell->line_curpos]);

        /* move the cursor to new position */
        for (i = shell->line_curpos; i < shell->line_position; i++) {
            SHELL_PRINTF("\b");
        }
    } else {
        shell->line[shell->line_position] = data;
        SHELL_PRINTF("%c", data);
    }
    //position curpos分别 +1后移
    data = 0;
    shell->line_position++;
    shell->line_curpos++;

}

上段代码中shell是struct shell的全局变量,其结构体如下

struct shell {
    //输入数据状态,有鼠标左键,和功能键,其中功能键前面需要加0x5b查询资料可以得到其对应的是windos键,支持windows + up/down/left/right等操作,up/down显示的是显示前后命令记录,left/right这个看样子功能应该是左右移动光标
    enum input_stat stat;

    //当前的shell历史记录index值
    uint16_t current_history;
    //shell历史记录总数
    uint16_t history_count;
    //存放历史记录的二维数组,最多有5条历史记录,每条记录命令字符串(包含参数)一共120个字符
    char cmd_history[SHELL_HISTORY_LINES][SHELL_CMD_SIZE];
    //当前shell的命令及参数数据
    char line[SHELL_CMD_SIZE];
    //输入shell字符串的长度,(命令+参数)
    uint16_t line_position;
    //当前输入的位置,配合left/right快捷键可以实现类似windos上的前后插入功能
    uint16_t line_curpos;

#ifdef SHELL_USING_AUTH
    //shell 加密密码
    char password[SHELL_PASSWORD_MAX];
#endif

#if defined(SHELL_USING_FS)

#endif
    //自定义shell printf
    void (*shell_printf)(char *fmt, ...);
};

现在知道了只有在数据结尾输入\r或者\n才会调用shell_exec实现真正的执行代码,所以也需要在串口调试助手发送设置里勾选结尾发送回车换行符(每个助手设置方式可能不太一样)

shell_exec执行命令

shell_exec函数主要有一下4个任务:

  • 去掉了命令前面的空格和tab键
  • 执行shell_exec_cmd函数,拿到返回值并判断是否执行成功
  • 如果定义了SHELL_USING_LWIP宏,上面shell_exec_cmd执行失败就会去执行shell_exec_lwp函数,通过系统调用exec来执行代码,这部分代码里面涉及到了open函数,推测是支持执行其他路径下的程序的,也就是可以通过把编译好的程序下载到文件系统中,可以通过路径和文件名去执行其他的程序,由于这部分没有源码无法正常分析,这部分功能只能是推测了,期待有大佬可以解答
  • 前面2步如果都执行失败,则会打印出command not found

重点终于来了,最终分析到了执行shell命令的代码是shell_exec_cmd函数,该函数将会揭秘命令是如何和代码函数联系起来的,以及是怎么执行的,执行的步骤主要有下面几个

  • 通过字符串比较查找第一个空格来区分命令和参数
  • 通过shell_get_cmd函数根据命令字符串转cmd_function_t函数指针
  • 通过shell_split函数根据空格把命令后的参赛字符串分割成一个个字符串(也就是把一个个参数放入argv数组中)
  • shell_start_exec执行函数

下面一个个分析代码

字符串提取命令名字

shell命令的基本格式command [option] <args>

根据shell命令的使用格式很容易想到找到从第一个非空格的字符到第一个空格或结尾中间的字符就是命令本体

通过while循环不断的对比字符串是否是空格或者是tab很容易就找到command本体(前面以及处理过开头的空格和tab,cmd的第一个字符肯定是有效字符)

/* find the size of first command */
    while ((cmd[cmd0_size] != ' ' && cmd[cmd0_size] != '\t') &&
           cmd0_size < length) {
        cmd0_size++;
    }
根据command获取到cmd_function_t函数指针

前面我们以及分析过了,所有shell命令都被编译器放在FSymtab段中了,而且也知道了该段的起始和结尾地址,并且分别赋值给了_syscall_table_begin 和 _syscall_table_end两个对象,这两个对象也都是shell_syscall结构体对象,而这个结构体对象前面我们也讲了其前4个字节就是shell命令注册的name字段,只要我们遍历_syscall_table_begin和_syscall_table_end这两个地址之间的数据,拿到其name字符串和shell输入command来对比是否一样,如果一样就返回结构体中的后4个字节的数据也就是cmd_function_t指针,这样就完成了command和cmd_function_t函数指针的转换,下面是具体的代码及注释

static cmd_function_t shell_get_cmd(char *cmd, int size)
{
    struct shell_syscall *index;
    cmd_function_t cmd_func = NULL;

    /**
     * 遍历begin和end两个地址直接的数据,并且赋值给shell_syscall结构体index,
     * 这样index++就会自行跳过sizeof(struct shell_syscall)个字节,这两个地址本质上和数组没区别
     */ 
    for (index = _syscall_table_begin; index < _syscall_table_end; index++) {
        //判断是否符合shell注册时的name格式(__fsym___cmd_*_name, 这里的*指的是command本体)
        if (strncmp(index->name, "__cmd_", 6) != 0) {
            continue;
        }
        //比较cmd和shell_syscall结构体的名字是否一致,一致就赋值给cmd_func函数指针并返回
        if (strncmp(&index->name[6], cmd, size) == 0 &&
            index->name[6 + size] == '\0') {
            cmd_func = (cmd_function_t)index->func;
            break;
        }
    }

    return cmd_func;
}
根据空格分隔命令参数

这部分和获取command本体基本一样,不同的参数是可以有多个的(代码定义最多有16个),所以一直对比到字符串结尾才可以,而command只需要找到第一个不是空字符串的位置即可,这部分代码如下

static int shell_split(char *cmd, uint32_t length, char *argv[SHELL_ARG_NUM])
{
    char *ptr;
    uint32_t position;
    uint32_t argc;
    uint32_t i;

    ptr = cmd;
    position = 0;
    argc = 0;

    while (position < length) {
        //根据空格或tab来分割一个参数
        /* strip bank and tab */
        while ((*ptr == ' ' || *ptr == '\t') && position < length) {
            *ptr = '\0';
            ptr++;
            position++;
        }
        //处理最大值
        if (argc >= SHELL_ARG_NUM) {
            SHELL_E("Too many args ! We only Use:\r\n");

            for (i = 0; i < argc; i++) {
                SHELL_E("%s ", argv[i]);
            }

            SHELL_E("\r\n");
            break;
        }

        if (position >= length) {
            break;
        }
        //支持英文双引号参数格式
        /* handle string */
        if (*ptr == '"') {
            ...
        } else {
            //复制到argv数组中
            argv[argc] = ptr;
            argc++;
            //跳过掉多余的空格和tab,避免多个空格或tab连在一起
            while ((*ptr != ' ' && *ptr != '\t') && position < length) {
                ptr++;
                position++;
            }

            if (position >= length) {
                break;
            }
        }
    }

    return argc;
}
执行cmd_function_t函数

前面以及拿到了command本体、argv参数数组以及cmd_function_t函数指针,有了函数指针只需要调用函数指针并传入对应的参数即可,这部分代码如下

//调用start_exec执行函数指针

*retp = shell_start_exec(cmd_func, argc, argv);

__attribute__((weak)) int shell_start_exec(cmd_function_t func, int argc, char *argv[])
{
    //调用函数指针执行函数本体
    return func(argc, argv);
}

shell执行流程的全部代码如下

static int shell_exec_cmd(char *cmd, uint32_t length, int *retp)
{
    int argc;
    uint32_t cmd0_size = 0;
    cmd_function_t cmd_func;
    //shell 命令参数,最大值16
    char *argv[SHELL_ARG_NUM];

    // ASSERT(cmd);
    // ASSERT(retp);

    //找到command本体
    /* find the size of first command */
    while ((cmd[cmd0_size] != ' ' && cmd[cmd0_size] != '\t') &&
           cmd0_size < length) {
        cmd0_size++;
    }

    if (cmd0_size == 0) {
        return -1;
    }

    //获取cmd_function_t函数指针
    cmd_func = shell_get_cmd(cmd, cmd0_size);

    if (cmd_func == NULL) {
        return -1;
    }

    /* split arguments */
    //根据空格分割一个个参数,并且复制到argv数组里面
    memset(argv, 0x00, sizeof(argv));
    argc = shell_split(cmd, length, argv);

    if (argc == 0) {
        return -1;
    }

    /* exec this command */
    shell_signal(SHELL_SIGINT, SHELL_SIG_DFL);
    shell_dup_line(cmd, length);
    //执行函数
    *retp = shell_start_exec(cmd_func, argc, argv);
    // *retp = cmd_func(argc, argv);
    return 0;
}

手写kill命令

前面分析了shell命令是如何初始化及注册shell命令的,写自己的shell命令分为下面几步

  • 引入shell.h头文件
  • 创建cmd_function_t函数也就是int (*cmd_funtion_t)(int argc, char **argv)
  • 在文件中(非函数体中)调用SHELL_CMD_EXPORT_ALIAS并传入相关参数

接下来我们一步一步来写

引入shell.h头文件

首先创建一个task_kill.h文件,并且引入shell.h

#ifndef __TASK_KILL_H
#define __TASK_KILL_H

#include "shell.h"

#endif

创建task_kill.c源文件,其代码实现如下

#include <stdio.h>
#include "task_kill.h"
#include "FreeRTOS.h"
#include "task.h"

//cmd_function_t 函数实现体
int cmd_kill(int argc, char** argv)
{
    //处理shell参数
    if (argc != 2)
    {
        printf("usage: %s task name\n", argv[0]);
        return -1;
    }
    TaskHandle_t handle = NULL;
    //根据task name获取TaskHandle对象
    handle = xTaskGetHandle(argv[1]);
    if (handle == NULL)
    {
        printf("not found %s task\n", argv[1]);
        return -1;
    }
    //找到后就删除该函数
    vTaskDelete(handle);
    return 0;
}
//注册shell命令
SHELL_CMD_EXPORT_ALIAS(cmd_kill, kill, kill task);
  • 在main.c中引入task_kill.h头文件

由于是单独写在了task_kill.h文件中,所以需要在main.c中引入task_kill.h并且需要在CMakeLists.txt把对应的.c文件添加到源文件中。

上面就是kill命令的全部实现了,cmd_kill函数实现还是很简单的,先通过taskName找到TaskHandle,然后就可以通过TaskHandle来删除task了

最后验证

如果task_kill.c正常编译打开build/build_out/shell_demo_*.asm中可以找到__fsym___cmd_kill字样

a00a258c <__fsym___cmd_kill>:
a00a258c:   9e44 a00b 0fd8 a00a                         D.......

a00a2594 <__fsymtab_end>:
a00a2594:   5b5d495b            0x5b5d495b

找到类似的代码就说明kill命令以及正常编译进最终的二进制文件中了

话不多说 直接make flash COMX=COM7(COM7是我本机上的端口)刷机验证,刷完固件重启后打开串口助手,先设置发送结尾添加回车换行符

回车换行符

输入help查看所有shell命令,串口输出如下

shell commands list:
free
ping
memtrace
help
ps
set_ipv4
wifi_raw_send
wifi_ap_conf_max_sta
wifi_ap_mac_get
wifi_ap_stop
wifi_ap_start
wifi_sta_del
wifi_sta_list
wifi_sta_info
wifi_sta_ps_off
wifi_sta_ps_on
wifi_sta_autoconnect_disable
wifi_sta_autoconnect_enable
wifi_sta_mac_get
wifi_sta_ssid_passphr_get
wifi_sta_channel
wifi_sta_rssi
wifi_state
wifi_sniffer_off
wifi_sniffer_on
lwip
wifi_sta_disconnect
wifi_sta_connect
wifi_scan
phy
hello
reboot
kill

可以看到最后打印出了kill命令,说明已经实现了kill命令

自带的有32个命令,加上自己写的kill一共有33个命令,比如简单的reboot重启,wifi_stat获取wifi状态,以及wifi_ap_start/stop AP开启和关闭,wifi_ap_mac_get获取ap的mac地址,类似的还有wifi_sta系列和wifi连接相关的命令,wifi_sta_disconnect断开,wifi_sta_connect连接wifi,ps打印当前任务栈信息等等

接下来在串口助手输入ps查看所有的任务栈,输出信息如下:

Task         State   Priority  Stack    #          Base
********************************************************
shell_exec_task X   4   977 16
IDLE            R   0   77  5
test            B   10  16  4
WPA             B   26  1902    11
tcpip_thread    B   28  946 2
TX              B   29  938 9
Control         B   27  742 7
Tmr Svc         B   31  848 6
IPC task        B   29  410 12
WiFi task       B   27  1955    10
shell_task      B   5   858 1
RX              B   27  934 8

可以看到有一个test任务(main中创建的空任务,用来测试kill命令的)

然后在串口助手输入kill test,然后再输入ps查看所有的任务,最后得到ps的输出如下:

Task         State   Priority  Stack    #          Base
********************************************************
shell_exec_task X   4   977 27
IDLE            R   0   77  5
WPA             B   26  1902    11
tcpip_thread    B   28  946 2
Tmr Svc         B   31  848 6
IPC task        B   29  410 12
RX              B   27  934 8
WiFi task       S   27  1955    10
shell_task      B   5   858 1
TX              B   29  938 9
Control         B   27  742 7

可以看到test任务已经被kill掉了

至此已经验证kill命令的功能也正常,相信看了这个教程对shell已经不陌生了,也可以实现一个自己的shell命令了,文中涉及到的代码会上传到gitee上方便大家下载查看,下载代码

最后

细节的地方可能理解不到位有讲解不对的地方还请大佬轻喷,详细的代码可以去SDK目录里面找找看,欢迎大佬们评论交流,共同进步,水积分拿到更多的开发板

本帖被以下淘专辑推荐:

回复

使用道具 举报

bzhou830 | 2023-12-19 16:52:24 | 显示全部楼层
点赞
选择去发光,而不是被照亮
回复

使用道具 举报

lovzx | 2023-12-19 17:07:04 | 显示全部楼层

点保存点成发布了,调整好了代码格式,刷新下看着更舒服
回复 支持 反对

使用道具 举报

lovzx | 2023-12-19 17:08:08 | 显示全部楼层

我也是菜鸡,相互学习
回复 支持 反对

使用道具 举报

爱笑 | 2023-12-19 17:11:12 | 显示全部楼层
用心做好保姆工作
回复

使用道具 举报

WT_0213 | 2023-12-20 09:15:56 | 显示全部楼层
学习了
回复

使用道具 举报

lovzx | 2023-12-20 10:42:08 | 显示全部楼层

相互学习
回复 支持 反对

使用道具 举报

1084504793 | 2023-12-20 11:23:21 | 显示全部楼层
学到了
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则