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