OpenOCD 代码学习(2)执行命令

概述

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

1 从 openocd_thread() 开始

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

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

    • (1)通过 parse_cmdline_args() 解析 openocd 时的命令行参数,如通过 -d3 指令日志级别,-f filename 指定配置文件等等

    • (2)通过 server_preinit() 函数进行 Server 的预初始化。

    • (3)通过 parse_config_file() 函数解析配置文件同时运行一些命令。

    • (4)通过 server_init() 函数对 Server 进行初始化。

    • (5)通过 command_run_line(ctx, "init") 执行 init 命令。不要小看这一句话,其实里面有很多初始化逻辑。

    • (6)通过 server_loop() 函数开始接收 Telnet 的连接

    • (7)通过 server_quit() 函数退出 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() 函数的调用,即所有命令的执行最终回调到 jim_command_dispatch() 函数。

  • 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 属性的,虽然我们即使不看源码也应该早猜到了。

posted @ 2024-09-28 13:24  送南阳马生序  阅读(40)  评论(3编辑  收藏  举报