从头再来问题多

刘欢有一首歌叫《从头再来》,歌词的核心部分是“心若在,梦就在,天地之间还有真爱,看成败人生豪迈,只不过是从头再来...”a6029e862dbe60a789d5d2b0e62597af.jpeg

这是一首励志的好歌,鼓励人不屈不挠,遇到困难不要轻易放弃,要重整旗鼓,“从头再来”,再次出发,再试一次。

但是,这样的话说起来容易,实际做起来常常不那么简单。人生易老,时过境迁,很多事情容不得“从头再来”。这个道理对人生如此,对软件也是如此。

以格蠹的NDB调试器为例,如果使用时忘记了某个准备动作,比如给目标机供电,就开始调试会话,那么NDB虽然兴匆匆地跑起来了,但是在试图和目标机建立CoreSight会话时,就会因为无法读到标志寄存器(IDR)而失败。

Using NTP-DAPv2 interface with VID:PID=0x0d28:0x1588, serial=123600017738abcddf21bc1c2000410fa5a5a5a547454455
NTP-DAP: SWD  Supported
NTP-DAP: FW Version = 2.0.2
NTP-DAP: Serial# = 123600017738abcddf21bc1c2000410fa5a5a5a547454455
NTP-DAP: Interface Initialised (SWD)
SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0 nRESET = 1
NTP-DAP: Interface ready
clock speed 100 kHz
Error connecting DP: cannot read IDR

这时按理说,可以立刻插上电,然后点击工具条上的“重新启动”按钮,重新开始会话。

021149ec8700b7f4c6fac5e1766f4525.png

但是如果用户真的这么操作,老版本NDB并不能如预期地那样“从头再来”取得成功。而是重来失败,报告错误。

NTP: 
Failed to setup NTP connection(0xfffffffc).
Make sure turn on JTAG. Try Fn + 5 on remote controller.
Failed to setup KD proxy for 0xfffffffc. Session ends.
Restarting...
Starting new session...
Err:启动调试失败: -2147467259
Starting KD session type=usb,proto=ocd,opt=rxsb,targetname=gdk3
BUG: current_target out of bounds.
Failed to init probe target.Failed to setup NTP connection(0x80004005).
Make sure turn on JTAG. Try Fn + 5 on remote controller.
Failed to setup KD proxy for 0x80004005. Session ends.

这时的解决方案只能是先关闭NDB程序,然后再次打开。这个方案虽然也可以叫“从头再来”,但是这个“重来”的回头路有点走的太多了。

对于这个“重来失败”的问题,我一直想把它解决掉。曾安排格蠹的小伙伴来解决,我自己也曾忙里偷闲,发起过几次“闪电战”,但都没能攻克这个难题。

国庆长假,到办公室加班,有比较充足的时间,我又想起了这个问题,于是从昨天上午开始发起“专项行动”,专门解决这个问题。以下是主要的战斗经过,分享给格友们。

从哪里下手呢?首先要有一个高效的环境。考虑到要反复“重来”,所以每次重来的速度要快。因为此,我选择了最轻便的GDK3做为调试目标,它具有如下优点:

- 不需要启用调试模式,上电即默认启用了调试,随时准备着迎接调试器的到来,发自内心地“欢迎调试我”。

- 供电方便,只要插上USB线就好。

- 开机速度快,没有操作系统,启动时间在眨眼之间,不到1秒。

2e5dcc34a59f5d7500e563c1c1f98b8b.jpeg

选定了GDK3做为调试目标后,我很快就总结出了一个便捷的重现步骤:GDK3不要上电就开始调试会话,问题百分百重现,在重新开始的地方设置断点,断点命中时,给GDK3上电。这样,重现一次的时间开销只有几秒钟,可以快速复现问题,定位重启失败的原因。

有了重现环境和步骤后,另一个问题是如何调试。起初我是用NDB的正式图形界面,也就是NanoCode主程序。这时要调试的话,需要找到NDB的工作进程,因为NanoCode从Node.JS那里继承了多进程模型。

a957b94f698029a9499360a64064273e.png

找到进程ID后,便可以把Visual Studio附加到NDB进程。

为什么使用Visual Studio,而没有使用WinDBG呢?因为VS更适合看源代码。可以先用它把问题聚焦一下。

附加好VS后,便可以中断下来进行分析,但是不高效。为了提高效率,我连续两次点击重启,这时如果没有调试的话,NDB的界面会显示如下消息,表示NDB进程内发生了异常。

56bf90f20473daa47afb79f4c98bc5c6.png

如果有调试器,那么便会中断到调试器当中。这时便可以直接看到有问题的代码,不需要东找西找。

8deb130770f4e8566c63e7aaea779a2d.png

异常发生在OpenOCD(简称OCD)的开源代码中。看起来是连续两次“从头再来”把OCD的内部逻辑搞乱套了,触发了空指针。

为了能有序的“从头再来”,我认真树立了和OCD的接口逻辑,特别是重来前的“清理逻辑”。因为第一次操作虽然失败,但是却改变了很多状态,留下了很多痕迹,重来前需要清理好,该释放的要释放。

OCD提供了一个名叫target_quit()的函数,是用来清理的,但是原来的代码根本没有调用。OCD自己的主程序在退出时也没有调用。

e66b19f2a95d4ab3ccf2026450e417f3.png

但当我把这个target_quit()调用加到清理函数后,我明白为什么没有人调用了。因为一调用就促发崩溃。异常发生现在下面的free函数,也就是释放内存。

bc2f83c60718d29ee94743ee113183c1.png

提到释放内存出错,很多程序员都会头疼。因为涉及到纷繁复杂,而又很容易出错的堆。

对于我来说,有太多方法对付内存出错,所以并不紧张。

当我看到这个释放调用的目标时,我很快明白这个问题的根源了:野指针。

所谓野指针,是从堆管理器的角度说的。正常的指针都是堆管理器发出去的,从堆上分的,交给堆释放。而野指针就是把不是堆上的指针给堆管理器释放。

free(target->type); 

从先是从target->type这个参数的语义上看出问题。然后再通过VS的变量观察功能分析,可以看到target->type指向的是全局变量。

5fd0c3b850895189ba0fb5007ced381a.png

确定问题后,只要把它注释掉,再加个注释。

// free(target->type); //badbad, type is global, should not call free

完善了清理代码后,无论点多少次“重启”都不会崩溃了。但是重启之后仍然不能成功建立会话。下面是重来时NDB打印的消息。

NTP: 
Failed to setup NTP connection(0xfffffffc).
Make sure turn on JTAG. Try Fn + 5 on remote controller.
Failed to setup KD proxy for 0xfffffffc. Session ends.
Restarting...
Starting new session...
Err:启动调试失败: -2147467259
Starting KD session type=usb,proto=ocd,opt=rxsb,targetname=gdk3
'target init' has already been called
BUG: current_target out of bounds.
Failed to init probe target.Failed to setup NTP connection(0x80004005).
Make sure turn on JTAG. Try Fn + 5 on remote controller.
Failed to setup KD proxy for 0x80004005. Session ends.

上面消息中,下面这句最关键:

'target init' has already been called

这句话的含意是'target init'已经做过一次了。是啊,重来么,是做过一次的。做过一次就不能再做么?

找到对应的源代码,又是在OCD中,因为很有代表意义,摘录如下:

static bool target_initialized;
if (target_initialized) {
    LOG_INFO("'target init' has already been called");
    return ERROR_OK;
}
target_initialized = true;

熟悉C语言的读者很容易看懂,这个函数故意定义了一个静态变量,执行一次后,就把这个变量设为true,第二次看到true,就不往下走,返回了。

这是故意的不支持“重来”啊。这不是技术水平问题,这是态度问题,从思想深处就没想到要支持“从头再来”。

考虑到这样的代码具有教育意义,我没有把它直接删掉,而是加了个条件编译,将其禁止。

#ifdef _TRY_RESTART_FAILURE
  static bool target_initialized;
  if (target_initialized) {
    LOG_INFO("'target init' has already been called");
    return ERROR_OK;
  }
  target_initialized = true;
#endif

禁止这个代码后,再次重现问题,得到下面的错误:

found
 \\?\d:\work\nano\nd\ndi\x64\debug\data\ntp.cfg
command - adapter driver NTP-DAP
command - adapter driver NTP-DAP
Interface already configured, ignoring

找到对应的源代码,也是在OCD中。考虑到它具有另一种代表意义,也摘录如下:

/* check whether the interface is already configured */
  if (adapter_driver) {
    LOG_WARNING("Interface already configured, ignoring");
    return ERROR_OK;
  }

与前面判断静态变量不同,这次判断的是全局变量。

struct adapter_driver *adapter_driver;

 看来是上次操作时设置了这个全局变量。

解决方法也比较简单,找到有关的清理函数——adapter_quit,在里面增加复位操作。

int adapter_quit(void)
{
  if (jtag && jtag->quit) {
    /* close the JTAG interface */
    int result = jtag->quit();
    if (ERROR_OK != result)
      LOG_ERROR("failed: %d", result);
  }


  struct jtag_tap *t = jtag_all_taps();
  while (t) {
    struct jtag_tap *n = t->next_tap;
    jtag_tap_free(t);
    t = n;
  }
  __jtag_all_taps = NULL; // added by Raymond while enableing restart on 2022-10-6
  jtag = NULL; // also this line.
  adapter_driver = NULL;


  return ERROR_OK;
}

类似这样通过判断全局变量拒绝重来的代码还有好几次。为了对本来美欧清理函数的全局变量做复位,我增加了一个清理函数,在这个清理函数里把全局变量复位。

// cleanup for restart, added by Raymond on 2022-10-6
void transport_cleanup(void)
{
  allowed_transports = NULL;
  session = NULL;
}

调用上述清理函数后,“重来”的过程显然走的更深入了,但仍不能工作。每次解决调一个问题,以为能工作时,常常是又出现一个新的问题。

调试的时间过得很快,昨天到了晚饭时间时,仍有问题。

昨天晚饭后,继续调试。有了白天的经验,晚上我改用Node+js脚本做目标进程,代码里判断第一次启动失败后,自动发起第二次调用。这比NDB图形界面又高效了一些。

var ret = obj.StartKD("type=usb,proto=ocd,opt=rx,targetname=gdk3", "");
console.log(ret);
if (ret != 0) {
    ret = obj.StartKD("type=usb,proto=ocd,opt=rxs,targetname=gdk3", "");
    console.log(ret);
}

这样调试后,比较快定位到了白天没有找到的一个问题。在比较著名的解析命令行参数的开源代码options.c中,也有一个通过全局变量记录选项号的全局变量optind。

WINGETOPT_API extern int optind;    /* index of first non-option in argv      */

因为这个全局变量,第二次“重来”时,解析不到命令行选项,因为第一次执行时,optind已经到了所有选项的最大值。

为此,我加了个判断,将其复位。

int parse_cmdline_args(struct command_context *cmd_ctx, int argc, const char *argv[])
{
  int c;
  if(optind > 1)
    optind = 1; // reset it to 1, fixed by Raymond on 2022-10-6
  while (1) {
    /* getopt_long stores the option index here. */
    int option_index = 0;


    c = getopt_long(argc, argv, "hvd::l:f:s:c:", long_options, &option_index);

继续排除几个“抗拒重来”的问题后,点击“重新启动”按钮后,已经可以开始调试会话了。但是发起中断时,无法将目标中断下来。

今天上午继续调试中断失败的问题。发现NDB获取到的CPU个数为2,而GDK3其实只有一个CPU。中断(Break)时一定要收到所有CPU的Halted事件才能中断给用户,所以CPU数量出错,用户就看不到中断成功的界面。

bfd880416189f4c0df41b066e80ddf77.png

追查源头,NDB是从OCD的target_type->num变量获取CPU数量的。而target_type->num是根据TCL脚本中的target命令来增加计数。重来时执行了两遍脚本,所以num数量不对了。

找到原因后,只要在注释调的free下面,加一个清零动作。

// free(target->type); //badbad, type is global, should not call free
  target->type->num = 0; // reset to zero

加了这个修正后,“重来”操作可以顺利进入调试会话,而且中断也可以成功了。为期一天半的“专项整治”终于结束了。

归纳一下,以下不好的编码习惯会增加代码的死板性,影响“从头再来”:

第一名:使用函数内的静态变量记录标志,不执行第二遍。这样的写法,不支持函数外部复位状态,是影响“从头再来”的第一大障碍。

第二名:全局变量。全局变量在运行中难免会记录下运行痕迹,留下“岁月的痕迹”。因此,应该尽量减少全局变量的数量。为了能支持从头再来,最好给全局变量配备复位函数,在复位时将变量状态复位。

第三名:清理逻辑不完备。包括资源没有清理好,动态内存没有释放,全局状态没有恢复,清理函数没有认真测试等。

灵活性是软件的优良品格。好的代码应该是可以反复重来的,不仅能跑起来,还要能停下来,能优雅的退出,能轻松的“从头再来”。

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

60e21f9e95aa624b0a7ef2ccc28859a0.png

也欢迎关注格友公众号

8223611f68aff4d3fcbcd89afc489cc1.jpeg

约瑟夫环问题是一个经典的数学问题,其具体描述是:n个人围成一圈,从第一个人开始报数,报到m的人出圈,下一个人再从1开始报数,报到m的人出圈,以此类推,直到所有人出圈,求出所有人出圈的顺序。 解决该问题的一种常见方式是使用循环链表。具体步骤如下: 1. 创建一个循环链表,将n个人依次加入链表中。 2. 从第一个人开始报数,每报到第m个人就将其从链表中删除。 3. 将被删除的人的编号记录下来,并将其从链表中删除。 4. 重复步骤2和步骤3,直到链表为空,所有人都出圈。 下面是使用C++语言实现的代码: ```c++ #include <iostream> using namespace std; struct Node { int val; Node *next; Node(int x) : val(x), next(NULL) {} }; int josephus(int n, int m) { Node *head = new Node(1); Node *cur = head; for (int i = 2; i <= n; i++) { cur->next = new Node(i); cur = cur->next; } cur->next = head; Node *prev = cur; cur = head; int cnt = 0; while (cur->next != cur) { cnt++; if (cnt == m) { prev->next = cur->next; cnt = 0; cout << cur->val << " "; delete cur; cur = prev->next; } else { prev = cur; cur = cur->next; } } int ans = cur->val; delete cur; return ans; } int main() { int n = 7, m = 3; cout << josephus(n, m) << endl; // 输出:4 return 0; } ``` 在上述代码中,我们使用了一个结构体`Node`来表示链表中的节点。在函数`josephus`中,我们首先创建一个循环链表,然后从头节点开始进行报数。每当报到第m个人时,就将该人从链表中删除,并记录其编号。最后,当链表中只剩下一个节点时,该节点即为最后一个出圈的人,我们将其编号返回即可。 该算法的时间复杂度为O(nm),空间复杂度为O(n)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值