1
头图

概述

  • 上一篇文章我们学习了 OpenOCD 注册命令的过程,这一篇我们来年一下 OpenOCD 执行命令的逻辑。

1 从 openocd_thread() 开始

  • 1)从 main() 函数到 openocd_main() 函数,最后我们来看 openocd_thread() 函数的执行逻辑:

  • 2)openocd_thread() 的逻辑一共有 7 个步骤:

    • (1)通过 <font color=red>parse_cmdline_args()</font> 解析 openocd 时的命令行参数,如通过 -d3 指令日志级别,-f filename 指定配置文件等等
    • (2)通过 <font color=red>server_preinit()</font> 函数进行 Server 的预初始化。
    • (3)通过 <font color=red>parse_config_file()</font> 函数解析配置文件同时运行一些命令。
    • (4)通过 <font color=red>server_init()</font> 函数对 Server 进行初始化。
    • (5)通过 <font color=red>command_run_line(ctx, "init")</font> 执行 init 命令。不要小看这一句话,其实里面有很多初始化逻辑。
    • (6)通过 <font color=red>server_loop()</font> 函数开始接收 Telnet 的连接
    • (7)通过 <font color=red>server_quit()</font> 函数退出 OpenOCD
  • 3)接下来,让我们看看这些步骤都有哪些内容。

2 parse_cmdline_args() 与 parse_config_file()

  • 1)JetBrains CLion 的代码烧录原理:

  • 2)如果你使用过 CLion 开发并烧录 STM32 程序,那么在 CLion 进行代码烧录时,你会看到如下的命令:

    D:\_Hardware\OpenOCD\bin\openocd.exe
        -s D:\_Hardware\OpenOCD\share\openocd\scripts     
        -f board/openluat_air001.cfg
        -c "tcl_port disabled"        
        -c "gdb_port disabled"        
        -c "tcl_port disabled"        
        -c "program \"/path/to/xxx.elf\""
        -c reset                              
        -c shutdown
  • 3)这条命令最终就是执行的 parse_cmdline_args() 与 parse_config_file() 函数来进行代码烧录的,下面我们具体来看一下。

2.1 parse_cmdline_args()

  • 1)parse_cmdline_args() 解析命令行参数逻辑:

  • 2)查看 parse_cmdline_args() 函数代码逻辑的同时,也可以查看 OpenOCD 在命令行支持的参数有哪些:

    -h 选项:查看帮助。
    -v 选项:查看 OpenOCD 版本。
    -f 选项:添加配置文件。当我们给目标 MCU 下载程序时,需要指定协议、烧录算法等,就是通过该选项指定的配置文件提供给 OpenOCD 的
    -s 选项:指定 OpenOCD 搜索配置文件的目录。除了 OpenOCD 默认的 \share\openocd\scripts 目录外,我们还可以指定其它的搜索目录。
    -d 选项:指定日志级别。当我们在开发自已的 MCU 烧录算法出现 bug 时,大部分情况下需要通过观察日志来解决,此时可以使用 -d3 选项来打开 debug 日志
    -l 选项:指定日志输出文件。
    -c 选项:指定 OpenOCD 需要运行的命令。当烧录完成后,我们需要让 MCU 复位来运行代码,并退出 OpenOCD,此时可以通过 -c reset -c shutdown 来实现
  • 3)这里要额外关注一下 -f 与 -c 选项:

    • (1)如 -f board/xxx.cfg 会被封装成 script board/xxx.cfg,然后添加到 config_file_names 数组中等待解析。
    • (2)-c 选项则直接添加到 config_file_names 数组中等待执行。
  • 4)此时是不是已经可以看懂 CLion 的烧录命令了呢?

    D:\_Hardware\OpenOCD\bin\openocd.exe
        -s D:\_Hardware\OpenOCD\share\openocd\scripts     
        -f board/openluat_air001.cfg
        -c "tcl_port disabled"        
        -c "gdb_port disabled"        
        -c "tcl_port disabled"        
        -c "program \"/path/to/xxx.elf\""
        -c reset                              
        -c shutdown
    • openocd 通过 -s 指定搜索配置文件目录,-f 指定 MCU 配置文件,-c 指定待执行的命令(其中的 program 即为烧录命令)

2.2 parse_config_file()

  • 1)通过上述 parse_cmdline_args() 函数解析完命令行参数,我们知道 OpenOCD 通过 -f 添加的配置文件以及 -c 添加的命令,最终都会保存到 config_file_names 数组中。接下来我们来看一下解析配置文件的 parse_config_file() 函数:

  • 2)可以看到 parse_config_file() 函数有 2 部分:

    • (1)如果我们不指定配置文件,则 OpenOCD 将使用默认的 openocd.cfg 文件。
    • (2)通过 command_run_line() 函数循环遍历 config_file_names 数组中的配置文件及命令。
  • 3)我们从 command_run_line() 函数开始一路向下跟踪来到 Jim_EvalObj() 函数(这里我删除了大部分代码,只留下主要逻辑),可以看到:

    int Jim_EvalObj(Jim_Interp *interp, Jim_Obj *scriptObjPtr)
    {
        ScriptObj *script;
    
        /* Execute every command sequentially until the end of the script
         * or an error occurs.
         */
        for (i = 0; i < script->len && retcode == JIM_OK; ) {
    
            /* Populate the arguments objects.
             * If an error occurs, retcode will be set and
             * 'j' will be set to the number of args expanded
             */
            for (j = 0; j < argc; j++) {
    
                switch (token[i].type) {
                    case JIM_TT_EXPRSUGAR:
                        retcode = Jim_EvalExpression(interp, token[i].objPtr);
                        break;
                    case JIM_TT_DICTSUGAR:
                        wordObjPtr = JimExpandDictSugar(interp, token[i].objPtr);
                        break;
                    case JIM_TT_CMD:
                        retcode = Jim_EvalObj(interp, token[i].objPtr);
                }
    
            if (retcode == JIM_OK && argc) {
                /* Invoke the command */
                retcode = JimInvokeCommand(interp, argc, argv);
                /* Check for a signal after each command */
                if (Jim_CheckSignal(interp)) {
                    retcode = JIM_SIGNAL;
                }
            }
        }
        
        return retcode;
    }
    • (1)对于 Jim_EvalObj() 函数,eval 这个词一般表示将指定的字符串作为代码执行,如果来源不受信,可能引起注入攻击。
    • (2)通过 switch 中的几个 case 来递归执行脚本文件中的命令
    • (3)最终通过 JimInvokeCommand() 函数来执行命令。
  • 4)接下来来到 JimInvokeCommand() 函数,别看这个函数有很多逻辑,其实最重要的就一句代码:

    retcode = cmdPtr->u.native.cmdProc(interp, objc, objv);
    • (1)还记得我们在 《OpenOCD 代码学习(一)注册命令》一篇中提到的命令注册逻辑吗?
    • (2)当时我们说过,注册命令的过程是:创建一个 commnad 实例,并且将其 u.native.cmdProc 属性赋值为 jim_command_dispatch() 函数,最后保存到 JIm Hash 中。
    • (3)结合 JimInvokeCommand() 函数的实现来看,cmdPtr->u.native.cmdProc() 函数调用,即是对 jim_command_dispatch() 函数的调用,即<font color=red>所有命令的执行最终回调到 jim_command_dispatch() 函数。</font>
  • 5)从 jim_command_dispatch() 函数于是 exec_command() 函数,再到 run_command() 函数,最终调用了 command 实例的 handler() 函数,再回头看一下一个命令 handler 是怎么声明的:

    static const struct command_registration telnet_command_handlers[] = {
        {
            .name = "exit",
            .handler = handle_exit_command,
            .mode = COMMAND_EXEC,
            .usage = "",
            .help = "exit telnet session",
        },
        {
            .name = "telnet_port",
            .handler = handle_telnet_port_command,
            .mode = COMMAND_CONFIG,
            .help = "Specify port on which to listen "
                "for incoming telnet connections.  "
                "Read help on 'gdb_port'.",
            .usage = "[port_num]",
        },
        COMMAND_REGISTRATION_DONE
    };
  • 6)至此,我们终于明白一个命令的执行是怎么回调到其 .handler 属性的,虽然我们即使不看源码也应该早猜到了。

送南阳马生序
7 声望3 粉丝

余之业有不精、德有不成,非天质之卑,则心不若他之专耳,岂他人之过哉!