南大PA思考题

思考题

攻略:https://www.cnblogs.com/nosae/p/17045249.html#测试

计算机可以没有寄存器吗?

如果没有寄存器, 计算机还可以工作吗? 如果可以, 这会对硬件提供的编程模型有什么影响呢?

理论上可以。有这些没有寄存器的架构:

  • Memory-Only Architecture: The computer would rely entirely on main memory or a cache-like structure to perform all operations.
    • Instructions would fetch operands directly from memory and write results back to memory.
    • Examples include early computers like the EDVAC or von Neumann architecture models before registers became commonplace.
  • Stack-Based Architectures: Stack machines (like early HP calculators) use a stack to manage data instead of registers.
    • Operations manipulate data on the top of the stack, reducing the need for explicitly addressed registers.

没有寄存器的架构会给硬件提供的编程模型带来更多限制,比如某些编程模型可能很难甚至无法在这种硬件架构的计算机上实现。

程序的状态机视角有什么好处?

有一些程序看上去很简单, 但行为却不那么直观, 比如递归. 要很好地理解递归程序在计算机上如何运行, 从状态机视角来看程序行为才是最有效的做法, 因为这一视角可以帮助你理清每一条指令究竟如何修改计算机的状态, 从而实现宏观上的递归语义.

一个程序从哪里开始执行?

为什么全部都是函数?

阅读init_monitor()函数的代码, 你会发现里面全部都是函数调用. 按道理, 把相应的函数体在init_monitor()中展开也不影响代码的正确性. 相比之下, 在这里使用函数有什么好处呢?

参数的处理过程

另外的一个问题是, 这些参数是从哪里来的呢?

究竟要执行多久?

在cmd_c()函数中, 调用cpu_exec()的时候传入了参数-1, 你知道这是什么意思吗?

谁来指示程序的结束?

在程序设计课上老师告诉你, 当程序执行到main()函数返回处的时候, 程序就退出了, 你对此深信不疑. 但你是否怀疑过, 凭什么程序执行到main()函数的返回处就结束了? 如果有人告诉你, 程序设计课上老师的说法是错的, 你有办法来证明/反驳吗? 如果你对此感兴趣, 请在互联网上搜索相关内容.

在c语言的视角里,程序从main开始,到main结束。

有始有终 (建议二周目思考)

对于GNU/Linux上的一个程序, 怎么样才算开始? 怎么样才算是结束? 对于在NEMU中运行的程序, 问题的答案又是什么呢? 与此相关的问题还有: NEMU中为什么要有nemu_trap? 为什么要有monitor?

monitor模块是为了方便地监控客户计算机的运行状态而引入的,它除了负责与GNU/Linux进行交互(例如读入客户程序)之外,还带有调试器的功能,为NEMU的调试提供了方便的途径。从概念上来说,monitor并不属于一个计算机的必要组成部分,但对NEMU来说, 它是必要的基础设施。如果缺少monitor模块, 对NEMU的调试将会变得十分困难。

读出状态机,知道状态机是怎么运行的。

如何测试字符串处理函数?

你可能会抑制不住编码的冲动: 与其RTFM, 还不如自己写. 如果真是这样, 你可以考虑一下, 你会如何测试自己编写的字符串处理函数?
如果你愿意RTFM, 也不妨思考一下这个问题, 因为你会在PA2中遇到类似的问题.

为什么printf()的输出要换行?

如果不换行, 可能会发生什么? 你可以在代码中尝试一下, 并思考原因, 然后STFW对比你的想法.

表达式生成器如何获得C程序的打印结果?

为什么要使用无符号类型? (建议二周目思考)

我们在表达式求值中约定, 所有运算都是无符号运算. 你知道为什么要这样约定吗? 如果进行有符号运算, 有可能会发生什么问题?

在C语言中,使用无符号类型进行表达式求值的约定,主要出于以下关键原因:

首先,无符号数在C语言中的溢出是定义良好的行为,按照模运算进行。

  1. 避免有符号整数的未定义行为

    • 溢出问题:有符号整数的溢出是未定义行为(UB)。例如,INT_MAX + 1 会导致UB,可能引发程序崩溃或不可预测的结果。
    • 移位操作:对有符号数的负值进行右移(如 -1 >> 1)是实现定义行为,不同编译器可能产生不同结果。
    • 无符号数的优势:无符号数的溢出和移位是明确定义的(按模运算和逻辑移位处理),更适合需要确定性行为的场景。
  2. 统一运算结果的解释

    • 除法/取余的确定性:无符号数的除法和取余结果始终为非负数,且操作符行为完全确定。例如:
      unsigned a = 5, b = 2;
      a / b = 2; a % b = 1; // 确定
      有符号数则需处理负数结果的截断方向(向零截断)和符号问题:
      int a = -5, b = 2;
      a / b = -2; a % b = -1; // 可能引发逻辑错误
    • 输出一致性:使用 %u 输出无符号数可避免有符号数到无符号数的隐式转换风险(如负数被误读为大正数)。
  3. 简化随机表达式的生成

    • 操作数范围可控:通过 gen_num() 生成的数字均为非负数(choose(100) 生成0~99),无符号类型天然适配这一约束。
    • 避免负号生成:代码未实现负号生成逻辑(如 gen('-') 仅用于运算符,而非一元负号),强制无符号可规避负数参与运算的复杂性。

若使用有符号运算可能引发的问题

  1. 未定义行为风险
    • 若生成的表达式导致有符号溢出(如 100 * 100 可能超过 INT_MAX),程序行为完全不可预测。
  2. 负数结果的歧义
    • 有符号运算可能产生负数,但输出使用 %u 会将其解释为无符号数。例如,表达式 3 - 5 结果为 -2,但输出会显示 4294967294(32位系统),这与直观预期不符。
  3. 除法/取余的边界问题
    • 有符号除法向零截断的特性可能导致意外结果(如 (-5)/2 = -2),而无符号除法直接截断((UINT_MAX -4)/2 行为明确)。

总结

使用无符号类型的核心目的是消除未定义行为确保运算确定性,并简化随机表达式生成的逻辑。若改用有符号类型,需额外处理负数生成、溢出检查、输出一致性等问题,显著增加代码复杂性和风险。在模糊测试或自动化生成的场景中,无符号约定是一种可靠的设计选择。

除0的确切行为

如果生成的表达式有除0行为, 你编写的表达式生成器的行为又会怎么样呢?

给编译器加-Wall-Werror参数,告诉编译器把所有warning当作error,即可过滤有生成的除0行为的测试样例。
同时检查fscanf()函数的返回值,若返回值不为1(无法成功读取result,可能因为除0错误),则跳过这轮循环。