我的应用程序程序员本能在调试汇编时失效了。
My application programmer instincts failed when debugging assembler

原始链接: https://landedstar.com/blog/posts/how-my-application-programmer-instincts-failed-when-debugging-assembler/

## 调试抽象层之下 作者利用最近的失业时间,尝试了Seiya Nuta的1000行操作系统教程,这与他们平时的高级应用编程有很大不同。这次经历凸显了一个关键的区别:在使用汇编和底层代码时,缺乏熟悉的抽象层。 调试比预期的更具挑战性。多年应用层调试的经验——追踪调用堆栈和调查逻辑错误—— оказались неэффективными。问题源于根本性的错误,例如缺少`ret`指令(导致意外的执行流程)以及打包结构体中不正确的数据类型大小。这些错误在C语言中编译,甚至*看起来*可以工作,但在汇编中由于手动偏移量计算而显现出来。 关键收获是一个严峻的认识:操作系统代码更接近硬件运行,需要直接检查汇编代码并关注内存布局。与高级语言不同,依赖抽象层来捕获错误的空间很小。虽然ChatGPT在RISC-V解释方面有所帮助,但它在调试方面遇到了困难,反映了作者最初以抽象为中心的调试方法。 尽管存在挑战,但这次经历令人鼓舞,与调试复杂应用框架的“魔法”相比,它提供了一种令人耳目一新的体验。

一位Hacker News用户分享了一篇关于调试汇编代码的文章,并指出作者即将获得一个关键的认识。评论中强调的核心观点是汇编语言*缺乏抽象性*。 与高级语言不同,汇编语言直接处理内存和寄存器,揭示了所有抽象最终都是模式——而非根本真理。这种对抽象的解构被认为是成为一名熟练黑客的关键,迫使人们深入理解事物*实际*运作方式。 评论者将其与物理学进行类比,认为即使像电子这样的概念也是对底层能量梯度的模型。最终,是“梯度贯穿始终”,甚至*这*本身也是一种抽象。评论者认为,这种理解是像Rowhammer这样的漏洞的基础,展示了超越表面层面的力量。
相关文章

原文

I’ve had a smidge of extra time with my recent unemployment, so to stay sharp and learn a few new things I followed Seiya Nuta’s guide to building an Operating System in 1,000 Lines.

I’m not an OS programmer, my life is normally spent at high-level application programming. (The closest I come to the CPU is the week I spent trying to internalize the flow of those crazy speculative execution hacks.) Assembler is easy enough to write, that wasn’t the problem. The problem was when I encountered problems. My years of debugging application-level code has led to a pile of instincts that just failed me when debugging assembler-level bugs.

These are the three places I had the biggest problems debugging.

I forgot the ret in a naked assembler function. It didn’t return to its caller.

What happened next is both fun and obvious—but only when you know that you missed a ret.

That function—let’s call it the first function—didn’t return to its caller, so execution just went to the next function in the file. The input arguments were whatever happened to be in the a0 and a1 registers. And when that second function returned, it used the caller information that was still available in the ra register, and it returned to where the first function was called from.

This is something that just doesn’t happen in application programming, which meant that I had a heck of a time debugging it.

It was even harder to debug because those two functions were related. They were next to each other in the file, of course they were related. I saw that the second function was doing strange stuff, and I was expecting it to be called around that time, so I focused on that error.

My application-programmer brain went like this: Why was it failing? It was sometimes being called with junk parameters, and it was being called more often than it should be. Why? Look at the caller. Why? Investigate the calling site. Investigate any loops. Move up the calling tree. Repeat. Repeat. Repeat. Which sent me nowhere near the problem. Everything went nowhere until I read the compiled assembler and started manually tracing execution.

Lesson 1: Application code is (mostly) about logical abstractions. OS code isn’t (always) about that. Debugging problems in OS code may be about just looking at adjacent assembler code.

(Addendum: This was around the process-creation code, which made things even weirder.)

Another error was an incorrect type inside a packed struct. It only needed 16 bits, but I was copying and pasting a previous line and gave it 32 bits.

In application programming, the size of the variable really doesn’t matter much to me, it’s almost entirely abstracted away in dynamic languages. I’ve spent a long time in the mindset that the size of types is on the other side of a certain abstraction, and that abstraction will nicely fail to compile if I make a mistake. I don’t think about it.

Types in C code are a lot more about how much space the variable takes up, with a bit of semantics on top. There’s no abstraction.

Even with one struct member having too much space allocated to it, the whole thing still compiled correctly, and all my tests in the C code showed it working.

But the struct was also being accessed in assembler. In assembler I was manually calculating the offsets from the struct location, using the sizes in the tutorial, and I didn’t make any silly mistakes while copying and pasting code here, which meant that suddenly that incorrect type caused a failure.

It was easy to printf and see that the values of the structs were correct, but that was C’s view of the struct.

Lesson 2 Lesson 1, again: There is no abstraction.

(Addendum: One thing I’ve learned about assembler code is that it just “goes forward” in a way that other languages don’t. In any pile of Rust code I have so many defined types and conversions and error handlers that errors are noted and bubble up right away. The nature of a good abstraction.)

I also learned how forgiving C parsing can be: __attribute((foo)) compiled and ran, even though the correct syntax is __attribute__((foo)). I got no compilation failure to tell me that anything went wrong.

Abstractions. They don’t exist in assembler. Memory is read from registers and the stack and written to registers and the stack.

It’s something that I know in my rational brain, and I was happily coding with that in mind. But when problems came up, I never realized how much I run on instinct and past patterns. I’ve been pretty good at debugging applications in my career, it’s what I’ve done most of. But my application-coded debugging brain kept looking at abstractions like they would provide all the answers. I rationally knew that the abstractions wouldn’t help, but my instincts hadn’t gotten the message.

I’m not an OS programmer or a low-level programmer. I don’t know if I’m sad about that, I like application-level programming. But it felt powerful to handle data on the stack directly.

It’s not that I love all levels of abstraction. Debugging a pile of assembler code is about reading the assembler code, which is nice. I enjoy that a lot more than the super-abstraction of Java Spring Boot, debugging a problem there looks a more like magic than programming (and eventually requires knowing a man named Will and texting him. Everyone should know a Will.)

But for everyone like me–the curious, the application programmers, and the unemployed–go ahead and do the Operating System in 1,000 Lines tutorial.

(Final note: ChatGPT was good at answering questions about RISC-V, but it was not good at finding bugs in code. It seemed to follow the logical-abstraction model of an application programmer and failed to help me with any of the above problems. But it was good at explaining the problems after I solved them.)

(Final final note: This post was written without ChatGPT, but for fun I fed my initial rough notes into ChatGPT and gave it some instructions to write a blog post. Here’s what it produced: Debugging Below the Abstraction Line (written by ChatGPT). It has a way better hero image.)

联系我们 contact @ memedata.com