“Troubleshooting”的版本间差异

来自osdev
跳到导航 跳到搜索
(创建页面,内容为“{{Tone}} == 提供基本的调试环境 == === 异常处理程序 === 第一件事是实现一个可靠的 “异常处理程序”,它会告诉你哪里出了问题。在像 Bochs 这样的仿真器下,如果仿真器被配置为这样做,则缺少这样的处理程序将导致 “没有解决的第三个异常” 紧急消息 (也就是 Triple Fault)。 在裸硬件上,它会简单地用短促的 “bip” 重置你的计算机。 每次CPU…”)
 
第2行: 第2行:
== 提供基本的调试环境 ==
== 提供基本的调试环境 ==
=== 异常处理程序 ===
=== 异常处理程序 ===
第一件事是实现一个可靠的 “异常处理程序”,它会告诉你哪里出了问题。在像 [[Bochs]] 这样的仿真器下,如果仿真器被配置为这样做,则缺少这样的处理程序将导致 “没有解决的第三个异常” 紧急消息 (也就是 [[Triple Fault]])。 在裸硬件上,它会简单地用短促的 “bip” 重置你的计算机。
第一件事是实现一个可靠的 “异常处理程序”,它会告诉你哪里出了问题。 在像[[Bochs]]这样的仿真器下,如果没有这样的处理程序,将会导致‘3rd Exception without resolution’的死机消息(也称为[[Triple Failure]])。 在真实硬件上,它会短促的 “bip”一声,然后简单地重置你的计算机。


每次CPU无法调用某个异常处理程序时,它都会尝试执行 [[Double Fault]] 异常处理程序。 如果它无法调用到异常处理程序,则会发生 [[Triple Fault]]。 还要记住,异常不能被掩盖,所以要么你的代码是完美的,要么你需要异常处理程序。 还请记住,当你运行应用程序 '''''它们的''''' 代码有自以为是完美的,没有异常处理程序,因为这样让他们的安静的快速运行很重要。
每次CPU无法调用某个异常处理程序时,它都会尝试执行 [[Double Fault]] 异常处理程序。 如果它无法调用到异常处理程序,则会发生 [[Triple Fault]]。 还要记住,异常不能被掩盖,所以要么你的代码是完美的,要么你需要异常处理程序。 还请记住,当你运行应用程序 '''''它们的''''' 代码有自以为是完美的,没有异常处理程序,因为这样让他们的安静的快速运行很重要。


让异常处理程序显示在尝试任何 “危险” 之前发生的异常非常方便。 例如,在屏幕的一角显示 (十六进制) 异常号可以节省调试时间。 :)
让异常处理程序能显示:在尝试任何 “危险” 之前发生的异常,是非常值得的。 例如,在屏幕的一角显示 (十六进制) 异常号可以节省调试时间。 :)


<!-- The following code example should be more generalized (e.g. exc_0d_handler, gpfExcHandler renamed to more meaningful names).-->
<!-- The following code example should be more generalized (e.g. exc_0d_handler, gpfExcHandler renamed to more meaningful names).-->
第14行: 第14行:
     mov gs,ZEROBASED_DATA_SELECTOR
     mov gs,ZEROBASED_DATA_SELECTOR
     mov word [gs:0xb8000],'D '
     mov word [gs:0xb8000],'D '
     ;; D in the top-left corner means we're handling
     ;; 左上角的D表示我们正在处理
     ;; a GPF exception right ATM.
     ;; 一个GPF异常权ATM。


     ;; 你的 “正常” 处理来到这里
     ;; 你的 “正常” 处理来到这里
第32行: 第32行:


     mov dword [gs:0xb8000],'  D-'
     mov dword [gs:0xb8000],'  D-'
     ;; the 'D' moved one character to the right, letting
     ;; “D”将一个字符向右移动,
     ;; us know that the exception has been handled properly
     ;; 让我们知道异常已得到适当处理
     ;; and that normal operations continues.
     ;; 正常的运作仍在继续。
     pop gs
     pop gs
     iret
     iret
</source>
</source>


一旦你实现了这样的技术,测试它可能是明智的,故意发出 “故障” 指令,看看是否显示了正确的代码。 在屏幕上的其他位置显示 “双重故障” 异常 (08) 也可能是明智之举。
一旦你实现了这样的技术,最好先测试它,故意发出 “故障” 指令,看看是否显示了正确的代码。 在屏幕上的其它位置显示 “double fault” 异常 (08) 也可能是明智之举。


==== 字符不能显示怎么办 ====
==== 字符不能显示怎么办 ====
例如,当您的GDT或分页表配置不当 (例如0xb8000不再指视频存储器) 时,就会发生这种情况。 幸运的是,视频内存并不是您与内核的唯一通信技术:
例如,当你的GDT或分页表配置不当 (例如0xb8000不再指视频存储器) 时,就会发生这种情况。 幸运的是,视频内存并不是你与内核的唯一通信技术:
* 您可以使用 [[PS2键盘 | 键盘]] LED报告某些事件 (例如,当您是处理程序时启用 “滚动锁定” LED,而在外出时将其禁用)。
* 你可以使用 [[PS2 Keyboard|键盘]] LED报告某些事件 (例如,当进入异常处理程序时点亮 “滚动锁定” LED,而在跳出异常处理时将其熄灭)。
* 您可以使用内部 [[PC扬声器]] 发出类似莫尔斯电码的信号,报告早期错误。(不过,如果编码较晚,可能会可耻。)
* 你可以使用内部 [[PC Speaker]] 发出类似莫尔斯电码的信号,报告早期错误。 (不过,如果编码太晚可能不太优雅。)
* 您可以使用 [[VGA硬件 | VGA寄存器]] 更改背景颜色或过扫描 (屏幕边框) 颜色以报告内核的当前 “状态”。(例如,黑色 = 正常操作,黄色 = 处理中断,红色 = 发生崩溃情况 (就在cli:hlt之前),蓝色 = 处理异常等...)
* 你可以使用 [[VGA Hardware|VGA寄存器]] 更改背景颜色或过扫描 (屏幕边框) 颜色以报告内核的当前 “状态”。 (例如,黑色 = 正常操作,黄色 = 处理中断,红色 = 发生崩溃情况 (就在cli:hlt之前),蓝色 = 处理异常等...)
参考 [[:Category:VGA | VGA资源]],看看你如何修改颜色。[[资源]] 页面应该包含LED闪烁和扬声器哔哔声的所有文档。
参考 [[:Category:VGA|VGA资源]],看看你如何修改颜色。[[resources|参考资源]] 页面应该包含LED闪烁和扬声器哔哔声的所有文档。
* 您可以通过 [[串行端口 | 串行端口]] 输出字节。大多数模拟器允许您将这些字符重定向到文件中,并且与屏幕不同,字符数不受限制。串行端口的驱动程序也很容易实现。
* 你可以通过 [[serial ports|串口]] 输出字节。 大多数模拟器允许你将这些字符重定向到文件中,与屏幕不同,输出字符数不受限制。 串口的驱动程序也非常容易实现。


==== 避免异常循环 ====
==== 避免异常循环 ====
所以我们知道什么时候发生异常,什么时候发生异常。 那更好,但仍然不是特别有用。 您的异常处理程序很可能会变得复杂,因为您的内核将发展,并且您会发现异常主要发生...在异常处理程序中。
所以我们知道什么时候发生异常,发生了什么异常。 这挺好,但有时候不是特别有用。 你的异常处理程序很可能会变得复杂,因为你的内核将持续发展,并且你会发现异常主要发生...异常处理程序本身中。


为了避免递归异常无休止地发生,您可以轻松地维护一个 “嵌套异常计数器”,每次输入异常处理程序时都会递增,并在离开该处理程序之前递减。 如果计数器高于几个单位的某个阈值 (3应该给出足够有趣的结果),内核将中止尝试解决异常并进入 “恐慌” 模式 (红色背景,闪烁的LED,无论如何)。
为了避免递归异常无休止地发生,你可以简单地维护一个 “嵌套异常计数器”,每次进入异常处理程序时都会递增,并在离开该处理程序之前递减。 如果计数器高于几个单位的某个阈值 (3应该给出足够有帮助的结果),考虑将内核中止尝试解决异常进入 “panic” 模式 (比如,红色背景,闪烁的LED)。


<source lang="c">
<source lang="c">
int nestexc = 0;
int nestexc = 0;


// called by the stub
// 由stub调用
void gpfExcHandler(void) {
void gpfExcHandler(void) {
   if (nestexc > MAX_NESTED_EXCEPTIONS) panic();
   if (nestexc > MAX_NESTED_EXCEPTIONS) panic();
第70行: 第70行:
</source>
</source>


你需要知道,当然,有些例外不是 “可恢复的”。 如果您的内核发出了一个零除法,则尝试返回 “div” 指令只会再触发一次异常 (是的!现在,现在 :)。 这样的循环不能通过 “嵌套” 计数器解决
你需要知道,当然,有些例外不是 “可恢复的”。 如果你的内核发出了一个除零异常,则尝试回到 “div” 指令只会再触发一次异常 。 这样的循环不能通过 “嵌套” 计数器解决


==== 显示堆栈内容 ====
==== 显示堆栈内容 ====
程序的大部分状态 (函数参数,返回地址,局部变量) 都存储在 [[堆栈]] 上,尤其是在使用C/C代码时。 一个完整的调试器 (如GDB) 将检查调试信息,为堆栈内容命名,提供调用列表等。 这对我们自己来说有点复杂,但是如果你的内核可以简单地 “显示” 堆栈的内容,如果你知道在代码中 “哪里” 这个过程停止了,你已经可以通过自己做调试器的工作来修复相当多的错误,猜测哪个堆栈位置包含哪个变量,返回地址在哪里等。
程序的大部分状态 (函数参数,返回地址,局部变量) 都存储在 [[stack|栈]] 上,尤其是在使用C/C++代码时。 一个完整的调试器 (如GDB) 将检查调试信息,为堆栈内容命名,提供调用列表等。 这对我们自己来说有点复杂,但是如果你的内核可以简单地 “显示” 堆栈的内容,如果你知道在代码中 “哪里” 这个过程停止了。以前你应该已经通过调试器的工作修复了相当多的错误,猜测哪个堆栈位置包含哪个变量,返回地址在哪里等。


堆栈内容仍在内存中。 错误处理的 [[EBP]] 值仍在内存中,并指向当前函数的堆栈帧的开始。 从这个地址到现在的所有东西都是当前的堆栈。 现在,您可以使用ebp中的值作为源。 只需使用以下调用:
堆栈内容仍在内存中。 错误处理的 [[EBP]] 值仍在内存中,并指向当前函数的堆栈帧的开始。 从这个地址到现在的所有东西都是当前的堆栈。 现在,你可以使用ebp中的值作为源。 只需使用以下调用:


<source lang="asm">
<source lang="asm">
第86行: 第86行:
</source>
</source>


and use <tt>void dump_hex(char *stack)</tt>.
并使用一个<tt>void dump_hex(char *stack)</tt>来转储栈信息。


==== 定位有故障的指令 ====
==== 定位有故障的指令 ====
在大多数情况下,当调用异常处理程序时,错误指令的地址在堆栈上的某个位置。 这里的第一步是打印出这个指令的地址。
在大多数情况下,当调用异常处理程序时,错误指令的地址在堆栈上的某个位置。 这里的第一步是打印出这个指令的地址。


完成此操作后,您 (作为人类) 可以检查 “链接器地图”,并找出问题出在哪个对象文件中。 You can request a map with <tt>ld </tt>''<usual options>''<tt> -Map </tt>''<filename.map>''.
完成此操作后,你 (作为人类) 可以检查 “linker map”,并找出问题出在哪个目标文件中。 你可以请求带有 <tt>ld </tt>''<常用选项>''<tt> -map </tt>''<filename.Map>'' 的映射。


<pre>
<pre>
第110行: 第110行:
</pre>
</pre>


是地图的一个例子。 如果错误地址为0x554f,则可以从中得知错误位于init.o中的某个位置, and most likely in <tt>kinit()</tt> function (it may still be in some other function if there are some ''static'' functions in the source file). All we know is that the error occurred at offset +21f in the file.
以上是一个映射的例子。 如果错误地址为0x554f,则可以从中得知错误位于init.o中的某个位置, 并且很可能在<tt>kinit()</tt>函数中 (如果源文件中有一些 “静态” 函数,则可能仍处于其它功能中)。 我们只知道错误发生在文件的偏移量+21f处。


Now, we can use <tt>objdump -drS bin/init.o</tt> to get a look at the disassembled output. 请注意,只有当您在那些分离的信息中启用了调试信息时,此步骤才能正常工作 <tt>.o</tt> files...
现在,我们可以使用<tt>objdump -drS bin/init.o</tt>查看分解后的输出。 请注意,只有当你启用了调试信息时,此步骤才能正常工作,在那些分离的 <tt>.o</tt> 文件中...


<source lang="c">
<source lang="c">
第129行: 第129行:
</source>
</source>


Of course, as I picked up a random address, there's nothing wrong to see at +21f, but I guess you got my point. :)
当然,当我拿到一个随机地址时,在+21F看起来没什么不对的,但我想你明白我的意思了。:)


==== 查找源代码的违规行 ====
==== 查找源代码的违规行 ====
第138行: 第138行:
</pre>
</pre>


=== 增强的调试技术 ===
=== 增强调试技术 ===
==== 堆栈跟踪 ====
==== 堆栈跟踪 ====
通过分析创建堆栈帧的默认方式,您可以一次撕掉一个堆栈帧,从而导致导致故障的调用序列。 对于单个奖励点,还可以提取参数并将其转储。 对于多个奖励点,请使用C名称mangling,并以正确的类型以可读形式导出参数。
通过分析创建堆栈帧的默认方式,你可以一次撕掉一个堆栈帧,从而引向导致故障的调用序列。 对于单个bonus point,还可以提取参数并将其转储。 对于多个bonus point,请使用C++名称mangling,并以正确的类型以可读形式导出参数。


每次调用函数时,它都会得到以下头/: ([[GCC]] 3.3.2)
每次调用函数时,它都会得到head/tail: ([[GCC]] 3.3.2)


<source lang="asm">
<source lang="asm">
第152行: 第152行:
</source>
</source>


在... 的地方...代码的其余部分已填写。 现在,如果你分析堆栈输出,它看起来像:
在... 的地方...代码的其余部分已填写。 现在,如果你分析堆栈输出,它看起来像这样:


<pre>
<pre>
0000FFC0 0000FFD0 -> this is the result of a push EBP (which pushes the esp at the start of the previous function)
0000FFC0 0000FFD0 -> 这是推送EBP的结果 (在上一个函数开始时推送esp)
0000FFC4 001023A5 -> this was the old EIP, which can be looked up in the function table (map file)
0000FFC4 001023A5 -> 这是旧的EIP,可以在函数表(映射文件)中查找
0000FFC8 01234567 -> this is an argument
0000FFC8 01234567 -> 这是一个参数
0000FFCC 89ABCDEF -> this is another argument
0000FFCC 89ABCDEF -> 这是另一个参数
0000FFD0 0000FFF0 -> this is again another EBP
0000FFD0 0000FFF0 -> 这又是另一个EBP
0000FFD4 00105BC3 -> this is again an EIP
0000FFD4 00105BC3 -> 这又是一个EIP
0000FFD8 001023A5 -> this is an argument (could be a function pointer)
0000FFD8 001023A5 -> 这是一个参数 (可能是函数指针)
0000FFDC 01234567 -> this is another argument
0000FFDC 01234567 -> 这是另一个参数
0000FFE0 89ABCDEF -> this is again an argument
0000FFE0 89ABCDEF -> 这又是一个参数
0000FFE4 000B8000 -> this is an argument, but not to this function
0000FFE4 000B8000 -> 这是一个参数,但不是这个函数的
0000FFE8 FFC00000 -> this is again an argument to a different function
0000FFE8 FFC00000 -> 这也是另一个函数的参数
0000FFEC 00010000 -> this is again another argument, but again not to this function.
0000FFEC 00010000 -> 这又是另一个参数,但同样不是这个函数的。
0000FFF0 0000FFFC -> this is the previous EBP again (note this is in this case the top)
0000FFF0 0000FFFC -> 这再次是以前的EBP (注意这是在这种情况下的顶部)
0000FFF4 0010002C -> this is an old EIP
0000FFF4 0010002C -> 这是一个旧的EIP
0000FFF8 00000001 -> this is an argument
0000FFF8 00000001 -> 这是一个参数
0000FFFC 00000000 -> this is the EBP at the start of the first function, not necessarily valid!
0000FFFC 00000000 -> 这是第一个函数开始时的EBP,不需要验证!
</pre>
</pre>


现在,您可以沿着执行的路径遍历。 EBP的内容是EBP的旧值,即最后一个堆栈帧之一。 上面的值是旧指令指针 (指向当前函数内部),上面的值 (直到但不包括旧EBP所指向的值) 是参数。 请注意,参数不必属于此函数,GCC偶尔会通过不弹出值来将add保存到esp。 然后假装旧的EBP是当前的EBP,您可以解除另一个呼叫。 这样做,直到你受够了它,你有足够的输出或堆栈结束。 如果是最后一个,请注意不要产生双重故障。
现在,你可以沿着执行的路径遍历。 EBP的内容是EBP的旧值,即最后一个堆栈帧之一。 上面的值是旧指令指针 (指向当前函数内部),上面的值 (直到但不包括旧EBP所指向的值) 是参数。 请注意,参数不必属于此函数,GCC偶尔会通过不弹出值来将add保存到esp。 然后假装旧的EBP是当前的EBP,你可以解除另一个call。 这样做,直到你推演的足够,你就有了足够的输出或堆栈结束。 如果是最后一个,请注意不要产生双重故障(double fault)。


如果使用C名称mangling,则参数将在函数名称中编码。 如果您可以阅读该内容,则可以解码堆栈上的值必须是什么,因此您实际上可以以普通函数调用的形式将其呈现给用户,并具有清晰的参数和所有内容。 这是堆栈转储方法的 “cr è me de la cr è me”,因此大多数人都不会这样做。
如果使用C++名称mangling,则参数将在函数名称中编码。 如果你可以阅读该内容,则可以解码堆栈上的值必须是什么,因此你实际上可以以普通函数调用的形式将其呈现给用户,并具有清晰的参数和所有内容。 这是堆栈转储方法的 “crème de la crème”(译者注:来自法语的成语-极品精粹),因此大多数人都不会这样做。


当我用C编程我的内核时,我实际上想到了编写一个脚本,该脚本将解析头文件的函数声明,使用 <tt>objdump</tt> 从编译的内核映像中提取调试符号,并编写一个系统映射将提供类型。 虽然爱上了 [[Bochs]] 的调试器后忘记了。 类似地,可以创建结构化类型的类型,这将允许GDB或 [[Visual Studio]] 提供给您的相同类型的浏览。 这将是精华。
当我用C编程我的内核时,我实际上想到了编写一个脚本,该脚本将解析头文件的函数声明,使用 <tt>objdump</tt> 从编译的内核映像中提取调试符号,并编写一个系统映射将提供类型。 可是在爱上了 [[Bochs]] 的调试器后就忘记了。 类似地,可以创建结构化类型的类型,这将允许GDB或 [[Visual Studio]] 提供给你的相同类型的浏览。 这才是 crème de la crème。


=== 调试技术 ===
=== 调试技术 ===
If your function <tt>x()</tt> wreaks havoc only after 1000 calls it may not suffice to put a <tt>panic()</tt> statement inside the functions to see where the functions breaks. 你可能想知道哪个调用是恶性的。 要做到这一点,可以使用全局或静态var来计算调用和panic() 之后的数额,看看它是否设法崩溃。 如果没有,则尝试两次该金额; 如果确实崩溃,则尝试平分以找到金额。
如果函数<tt>x()</tt>仅在1000次调用后才造成严重破坏,那么在函数中放入<tt>panic()</tt>语句来查看函数在哪里中断可能是不够的。 你可能想知道哪个调用是恶性的。 要做到这一点,可以使用全局或静态变量来计算多少次调用后panic(),看看它是否设法崩溃。 如果没有,则尝试双倍数量,如果这时崩溃发声,则尝试折半以找到数量范围。


<source lang="c">
<source lang="c">
第187行: 第187行:
     Z++;
     Z++;
     uint32_t N = 1000;
     uint32_t N = 1000;
     if (Z > N) panic();  //find the largest integer N for which it crashes not
     if (Z > N) panic();  // 查找不崩溃的最大整数N
     if (in_critical_section()) return;
     if (in_critical_section()) return;
     ...
     ...
}
}
</source>
</source>
...and then check how far does it go:
...然后检查它运行了多少:
<source lang="c">
<source lang="c">
Z++;
Z++;
第198行: 第198行:
                                       //we get here,
                                       //we get here,
if (in_critical_section()) return;
if (in_critical_section()) return;
if (Z > N) panic();                    //do we get here to panic() before a crash?
if (Z > N) panic();                    // 在崩溃前这里运行到panic()了吗?
</source>
</source>
However as complexity rises or multithreading is involved, it is less probable that a crash would be consistently occurring at the same point, after the same amount of calls every time. Then it would not be possible to find the number of the call to <tt>scheduler_choose_talk()</tt> that crashes it (because that number changes). Debugging needs some imagination; what if you knew, by tracing the program flow with <tt>print(__LINE__)</tt> that <tt>scheduler_choose_task()</tt> crashes only when a call to <tt>fun1()</tt> is in progress? You might use a global var <tt>uint32_t dbg</tt> or an array (<tt>uint32_t dbg[20]</tt>) of various <tt>dbg</tt> vars (which are used only in debugging code which is cleaned after the programmer ceases to debug) in a manner such as:
但是,随着复杂性的增加或涉及到多线程,在每次调用相同数量的情况下,在相同的点上持续发生崩溃的可能性较小。 然后,将无法找到使其崩溃的 <tt>scheduler_choose_talk()</tt> 调用的次数 (因为该次数发生了变化)。 调试需要一定的想象力; 如果你知道,通过使用 <tt>print(__LINE__)</tt> 跟踪程序流,<tt>scheduler_choose_task()</tt> 仅在执行 <tt>fun1()</tt> 时崩溃? 你可以使用全局变量 <tt>uint32_t dbg</tt> 或各种 <tt>dbg</tt> 变量的数组 (<tt>uint32_t dbg[20]</tt>) (仅在调试代码中使用,该变量在程序员停止调试后被清除),例如:
<source lang="c">
<source lang="c">
void fun1() {
void fun1() {
第210行: 第210行:
}
}
</source>
</source>
...and:
...以及:
<source lang="c">
<source lang="c">
void scheduler_choose_task() {
void scheduler_choose_task() {
第218行: 第218行:
}
}
</source>
</source>
(Or mix it with a call count, <tt>Z++; if (Z>5 && dbg[3] == 1) panic()</tt>.)
(或者将其与调用计数混合使用,<tt>Z++; if (Z>5 && dbg[3] == 1) panic()</tt>。)


Using the <tt>__LINE__</tt> aids tracing the program flow:
使用<tt>__LINE__</tt>帮助跟踪程序流程:
<source lang="c">
<source lang="c">
print(__LINE__);
print(__LINE__);
</source>
</source>
See [[C preprocessor#Uses for debugging|Uses for debugging]] for more info.
有关详细信息,请参阅[[C preprocessor#用于调试|C用于调试的预定义]]




== 外部援助 ==
== 外部援助 ==
到目前为止,我们假设内核包含调试所需的所有信息 (如符号名称等)。 但是,在生产阶段,通常会从最终的二进制对象中删除此信息以及大多数 “调试打印”。
到目前为止,我们假设内核包含调试所需的所有信息 (如符号名称等)。 但是,在生产阶段,通常会从最终的二进制目标中删除此信息以及大多数 “调试打印”功能。


尽管如此,人们仍然可以想象一个内核将配备串行通信代码并连接到另一台计算机,该计算机将具有所有 “删除” 信息 (like the symbols map, or the debugging-info featured intermediate binaries).
尽管如此,人们仍然可以考虑给内核配备串行通信代码并连接到另一台计算机,该计算机将具有所有 “被移除的” 信息 (比如符号映射,或者以中间二进制为特征的调试信息)


如果发生紧急情况,内核可以例如通过串行行发送 <tt>eip</tt> 的值,并期望辅助PC使用与该地址相对应的函数名称和行号进行回复 (或将其转储在辅助PC的屏幕上,这是一个选择的问题 :))。
如果发生紧急情况,内核可以例如通过串行发送 <tt>eip</tt> 的值,并期望辅助PC使用与该地址相对应的函数名称和行号进行回复 (或将其转储在辅助PC的屏幕上,看你的选择问题 :))。


=== 调试界面 ===
=== 调试界面 ===
''现在我们有很多关于哪里出了问题的信息...我们能要求更多吗? Mobius的调试外壳和Clicker的信息面板告诉我们什么...''
''现在我们有很多关于哪里出了问题的信息...我们能要求更多吗? Mobius的调试Shell和Clicker的信息面板告诉我们什么...''


''其他人是否知道允许黑客在发生崩溃时以交互方式探测系统状态的操作系统?''
''其他人是否知道允许黑客在发生崩溃时以交互方式探测系统状态的操作系统?''


:''Yes. Guess. Correct: AmigaOS. ;-) 它提供了一种模式,即使在系统进入Guru Meditation之后,也可以通过串行线进行调试。 That was possible because AmigaOS enjoyed a 256 / 512 kByte ROM image that could not get corrupted.'' - [[User:Solar|MartinBaute]]
: ''是的。猜猜对了正确: AmigaOS。;-) 它提供了一种模式,即使在系统进入Guru Meditation之后,也可以通过串行线进行调试。 这是可能的,因为AmigaOS拥有256/512kbyte的ROM映像,不会被破坏。'' - [[User:Solar|MartinBaute]]


''传统上,Unix系统将其状态写入/dev/core以进行离线guru meditation''
''传统上,Unix系统将其状态写入/dev/core以进行离线guru meditation''
第245行: 第245行:
== 另见 ==
== 另见 ==
=== 文章 ===
=== 文章 ===
* [[How Do I Use A Debugger With My OS]]
*[[How Do I Use A Debugger With My OS|如何在操作系统中使用调试器]]


[[Category:Troubleshooting]]
[[Category:Troubleshooting]]
[[de:Debugging]]
[[de:Debugging]]

2022年3月13日 (日) 14:08的版本

本文的语气或风格 可能无法反映整个wiki使用的百科全书式语气。 有关建议,请参阅 Wikipedia's article on tone

提供基本的调试环境

异常处理程序

第一件事是实现一个可靠的 “异常处理程序”,它会告诉你哪里出了问题。 在像Bochs这样的仿真器下,如果没有这样的处理程序,将会导致‘3rd Exception without resolution’的死机消息(也称为Triple Failure)。 在真实硬件上,它会短促的 “bip”一声,然后简单地重置你的计算机。

每次CPU无法调用某个异常处理程序时,它都会尝试执行 Double Fault 异常处理程序。 如果它无法调用到异常处理程序,则会发生 Triple Fault。 还要记住,异常不能被掩盖,所以要么你的代码是完美的,要么你需要异常处理程序。 还请记住,当你运行应用程序 它们的 代码有自以为是完美的,没有异常处理程序,因为这样让他们的安静的快速运行很重要。

让异常处理程序能显示:在尝试任何 “危险” 之前发生的异常,是非常值得的。 例如,在屏幕的一角显示 (十六进制) 异常号可以节省调试时间。 :)

exc_0d_handler:
    push gs
    mov gs,ZEROBASED_DATA_SELECTOR
    mov word [gs:0xb8000],'D '
    ;; 左上角的D表示我们正在处理
    ;; 一个GPF异常权ATM。

    ;; 你的 “正常” 处理来到这里
    pushad
    push ds
    push es
    mov ax,KERNEL_DATA_SELECTOR
    mov ds,ax
    mov es,ax

    call gpfExcHandler

    pop es
    pop ds
    popad

    mov dword [gs:0xb8000],'  D-'
    ;; “D”将一个字符向右移动,
    ;; 让我们知道异常已得到适当处理
    ;; 正常的运作仍在继续。
    pop gs
    iret

一旦你实现了这样的技术,最好先测试它,故意发出 “故障” 指令,看看是否显示了正确的代码。 在屏幕上的其它位置显示 “double fault” 异常 (08) 也可能是明智之举。

字符不能显示怎么办

例如,当你的GDT或分页表配置不当 (例如0xb8000不再指视频存储器) 时,就会发生这种情况。 幸运的是,视频内存并不是你与内核的唯一通信技术:

  • 你可以使用 键盘 LED报告某些事件 (例如,当进入异常处理程序时点亮 “滚动锁定” LED,而在跳出异常处理时将其熄灭)。
  • 你可以使用内部 PC Speaker 发出类似莫尔斯电码的信号,报告早期错误。 (不过,如果编码太晚可能不太优雅。)
  • 你可以使用 VGA寄存器 更改背景颜色或过扫描 (屏幕边框) 颜色以报告内核的当前 “状态”。 (例如,黑色 = 正常操作,黄色 = 处理中断,红色 = 发生崩溃情况 (就在cli:hlt之前),蓝色 = 处理异常等...)

参考 VGA资源,看看你如何修改颜色。参考资源 页面应该包含LED闪烁和扬声器哔哔声的所有文档。

  • 你可以通过 串口 输出字节。 大多数模拟器允许你将这些字符重定向到文件中,与屏幕不同,输出字符数不受限制。 串口的驱动程序也非常容易实现。

避免异常循环

所以我们知道什么时候发生异常,发生了什么异常。 这挺好,但有时候不是特别有用。 你的异常处理程序很可能会变得复杂,因为你的内核将持续发展,并且你会发现异常主要发生...异常处理程序本身中。

为了避免递归异常无休止地发生,你可以简单地维护一个 “嵌套异常计数器”,每次进入异常处理程序时都会递增,并在离开该处理程序之前递减。 如果计数器高于几个单位的某个阈值 (3应该给出足够有帮助的结果),考虑将内核中止尝试解决异常进入 “panic” 模式 (比如,红色背景,闪烁的LED)。

int nestexc = 0;

// 由stub调用
void gpfExcHandler(void) {
   if (nestexc > MAX_NESTED_EXCEPTIONS) panic();
   nestexc++;

   if (!fix_the_error()) {
     write_an_error_message();
   }
   nestexc--;
   return;
}

你需要知道,当然,有些例外不是 “可恢复的”。 如果你的内核发出了一个除零异常,则尝试回到 “div” 指令只会再触发一次异常 。 这样的循环不能通过 “嵌套” 计数器解决

显示堆栈内容

程序的大部分状态 (函数参数,返回地址,局部变量) 都存储在 上,尤其是在使用C/C++代码时。 一个完整的调试器 (如GDB) 将检查调试信息,为堆栈内容命名,提供调用列表等。 这对我们自己来说有点复杂,但是如果你的内核可以简单地 “显示” 堆栈的内容,如果你知道在代码中 “哪里” 这个过程停止了。以前你应该已经通过调试器的工作修复了相当多的错误,猜测哪个堆栈位置包含哪个变量,返回地址在哪里等。

堆栈内容仍在内存中。 错误处理的 EBP 值仍在内存中,并指向当前函数的堆栈帧的开始。 从这个地址到现在的所有东西都是当前的堆栈。 现在,你可以使用ebp中的值作为源。 只需使用以下调用:

stack_dump:
  push ebp
  mov ebp, esp
  call dump_hex
  pop ebp
  ret ; note that this is not going to work, but it should be here for completion.

并使用一个void dump_hex(char *stack)来转储栈信息。

定位有故障的指令

在大多数情况下,当调用异常处理程序时,错误指令的地址在堆栈上的某个位置。 这里的第一步是打印出这个指令的地址。

完成此操作后,你 (作为人类) 可以检查 “linker map”,并找出问题出在哪个目标文件中。 你可以请求带有 ld <常用选项> -map <filename.Map> 的映射。

.text           0x0000000000005330      0x556 bin/init.o
                0x0000000000005370                kinit_dsp_buffer
                0x0000000000005380                kinit_glocal_tag
                0x0000000000005400                kinit
                0x0000000000005830                kreset
 *fill*         0x0000000000005886        0xa 00
 .text          0x0000000000005890      0x66d bin/kalloc.o
                0x0000000000005ac0                kfree
                0x0000000000005b70                kmRegister
                0x00000000000059b0                kealloc
                0x0000000000005e60                kmFindP
                0x0000000000005dd0                kmFindA
                0x0000000000005d10                kmSetFull
                0x0000000000005890                kalloc

以上是一个映射的例子。 如果错误地址为0x554f,则可以从中得知错误位于init.o中的某个位置, 并且很可能在kinit()函数中 (如果源文件中有一些 “静态” 函数,则可能仍处于其它功能中)。 我们只知道错误发生在文件的偏移量+21f处。

现在,我们可以使用objdump -drS bin/init.o查看分解后的输出。 请注意,只有当你启用了调试信息时,此步骤才能正常工作,在那些分离的 .o 文件中...

#ifdef __DEBUG__
  kprint("kernel in debug-mode(%x) press [SHIFT+SPACE] to bypass anykey()\n",
 216:   83 c4 f8                add    $0xfffffff8,%esp
 219:   a1 00 00 00 00          mov    0x0,%eax
                        21a: R_386_32   DbMsk
 21e:   50                      push   %eax
 21f:   68 a0 01 00 00          push   $0x1a0
                        220: R_386_32   .rodata
 224:   e8 fc ff ff ff          call   225 <kinit+0x155>
                        225: R_386_PC32 kprint
         DbMsk);
#endif

当然,当我拿到一个随机地址时,在+21F看起来没什么不对的,但我想你明白我的意思了。:)

查找源代码的违规行

在上一步中找到错误指令的地址后,可以通过运行

addr2line -e <your_kernel.elf> <address of faulty instruction>

增强调试技术

堆栈跟踪

通过分析创建堆栈帧的默认方式,你可以一次撕掉一个堆栈帧,从而引向导致故障的调用序列。 对于单个bonus point,还可以提取参数并将其转储。 对于多个bonus point,请使用C++名称mangling,并以正确的类型以可读形式导出参数。

每次调用函数时,它都会得到head/tail: (GCC 3.3.2)

push ebp
mov ebp, esp
...
leave
ret

在... 的地方...代码的其余部分已填写。 现在,如果你分析堆栈输出,它看起来像这样:

0000FFC0 0000FFD0 -> 这是推送EBP的结果 (在上一个函数开始时推送esp)
0000FFC4 001023A5 -> 这是旧的EIP,可以在函数表(映射文件)中查找
0000FFC8 01234567 -> 这是一个参数
0000FFCC 89ABCDEF -> 这是另一个参数
0000FFD0 0000FFF0 -> 这又是另一个EBP
0000FFD4 00105BC3 -> 这又是一个EIP
0000FFD8 001023A5 -> 这是一个参数 (可能是函数指针)
0000FFDC 01234567 -> 这是另一个参数
0000FFE0 89ABCDEF -> 这又是一个参数
0000FFE4 000B8000 -> 这是一个参数,但不是这个函数的
0000FFE8 FFC00000 -> 这也是另一个函数的参数
0000FFEC 00010000 -> 这又是另一个参数,但同样不是这个函数的。
0000FFF0 0000FFFC -> 这再次是以前的EBP (注意这是在这种情况下的顶部)
0000FFF4 0010002C -> 这是一个旧的EIP
0000FFF8 00000001 -> 这是一个参数
0000FFFC 00000000 -> 这是第一个函数开始时的EBP,不需要验证!

现在,你可以沿着执行的路径遍历。 EBP的内容是EBP的旧值,即最后一个堆栈帧之一。 上面的值是旧指令指针 (指向当前函数内部),上面的值 (直到但不包括旧EBP所指向的值) 是参数。 请注意,参数不必属于此函数,GCC偶尔会通过不弹出值来将add保存到esp。 然后假装旧的EBP是当前的EBP,你可以解除另一个call。 这样做,直到你推演的足够,你就有了足够的输出或堆栈结束。 如果是最后一个,请注意不要产生双重故障(double fault)。

如果使用C++名称mangling,则参数将在函数名称中编码。 如果你可以阅读该内容,则可以解码堆栈上的值必须是什么,因此你实际上可以以普通函数调用的形式将其呈现给用户,并具有清晰的参数和所有内容。 这是堆栈转储方法的 “crème de la crème”(译者注:来自法语的成语-极品精粹),因此大多数人都不会这样做。

当我用C编程我的内核时,我实际上想到了编写一个脚本,该脚本将解析头文件的函数声明,使用 objdump 从编译的内核映像中提取调试符号,并编写一个系统映射将提供类型。 可是在爱上了 Bochs 的调试器后就忘记了。 类似地,可以创建结构化类型的类型,这将允许GDB或 Visual Studio 提供给你的相同类型的浏览。 这才是 crème de la crème。

调试技术

如果函数x()仅在1000次调用后才造成严重破坏,那么在函数中放入panic()语句来查看函数在哪里中断可能是不够的。 你可能想知道哪个调用是恶性的。 要做到这一点,可以使用全局或静态变量来计算多少次调用后panic(),看看它是否设法崩溃。 如果没有,则尝试双倍数量,如果这时崩溃发声,则尝试折半以找到数量范围。

void scheduler_choose_task() {
    static uint32_t Z=0;
    Z++;
    uint32_t N = 1000;
    if (Z > N) panic();  // 查找不崩溃的最大整数N
    if (in_critical_section()) return;
    ...
}

...然后检查它运行了多少:

Z++;
uint32_t N = 1000;
                                       //we get here,
if (in_critical_section()) return;
if (Z > N) panic();                    // 在崩溃前这里运行到panic()了吗?

但是,随着复杂性的增加或涉及到多线程,在每次调用相同数量的情况下,在相同的点上持续发生崩溃的可能性较小。 然后,将无法找到使其崩溃的 scheduler_choose_talk() 调用的次数 (因为该次数发生了变化)。 调试需要一定的想象力; 如果你知道,通过使用 print(__LINE__) 跟踪程序流,scheduler_choose_task() 仅在执行 fun1() 时崩溃? 你可以使用全局变量 uint32_t dbg 或各种 dbg 变量的数组 (uint32_t dbg[20]) (仅在调试代码中使用,该变量在程序员停止调试后被清除),例如:

void fun1() {
    dbg[3] = 1;
    ...
    if (x()) { dbg[3]=0; return; }
    ...
    dbg[3] = 0;
}

...以及:

void scheduler_choose_task() {
    //  if (dbg[3]==1) panic();         //check here.. a panic saves the day from crashing!
    if (in_critical_section()) return;
    if (dbg[3]==1) panic();             //check here.. it crashes
}

(或者将其与调用计数混合使用,Z++; if (Z>5 && dbg[3] == 1) panic()。)

使用__LINE__帮助跟踪程序流程:

print(__LINE__);

有关详细信息,请参阅C用于调试的预定义


外部援助

到目前为止,我们假设内核包含调试所需的所有信息 (如符号名称等)。 但是,在生产阶段,通常会从最终的二进制目标中删除此信息以及大多数 “调试打印”功能。

尽管如此,人们仍然可以考虑给内核配备串行通信代码并连接到另一台计算机,该计算机将具有所有 “被移除的” 信息 (比如符号映射,或者以中间二进制为特征的调试信息)。

如果发生紧急情况,内核可以例如通过串行发送 eip 的值,并期望辅助PC使用与该地址相对应的函数名称和行号进行回复 (或将其转储在辅助PC的屏幕上,看你的选择问题 :))。

调试界面

现在我们有很多关于哪里出了问题的信息...我们能要求更多吗? Mobius的调试Shell和Clicker的信息面板告诉我们什么...

其他人是否知道允许黑客在发生崩溃时以交互方式探测系统状态的操作系统?

是的。猜猜对了正确: AmigaOS。;-) 它提供了一种模式,即使在系统进入Guru Meditation之后,也可以通过串行线进行调试。 这是可能的,因为AmigaOS拥有256/512kbyte的ROM映像,不会被破坏。 - MartinBaute

传统上,Unix系统将其状态写入/dev/core以进行离线guru meditation

另见

文章

de:Debugging