ICS实验随笔
关于右移的问题
问题发现
在上机进行ICS实验中,有一个函数要求产生highbit到lowbit全为1的数字。一个相当简单的想法是直接将0xFFFFFFFF右移hinghbit+1位后再 移回来,再取反,便可以得到第0位到第highbit位的数字,然后再进行后续操作。
然而在这个问题中,遇到了一些问题,在此记录一下。
左右移的位数为负数
负数移位(如x >> -1)属于未定义行为,不同平台和编译器会产生不同结果,部分会在编译阶段报错,而在一些特定运算下,如x >> (m-n)中,可能会因为某些特殊赋值而出现负数,应当进行避免。
右移溢出
根据上面函数所需要求,一开始写出如下的函数:
1 | int bitMask(int highbit, int lowbit) |
然而,在测试调试时发现,当highbit==31时,函数值则会有问题。溯源后发现,highbit+1==32时,high的值一直为0xFFFFFFFF,并不符合预期的0x0。起初以为是自己逻辑有问题,于是进行了一下测试,首先把highbit+1替换为32:
1 | int bitMask(int highbit, int lowbit) |
然而出乎意料的是,此时high的值变为正常的0x0,此时也开始报了warning,但显然不看warning是一个好习惯(经典笑话,笑)。则考虑到32与highbit+1的差异性,一开始没考虑常量和变量的问题,以为是32是unsigned类型而另一个是int类型,由于数据类型的问题可能会存在一些区别,虽然感觉这种思路似乎很离谱,且并没有什么道理可言,但是还是测试了一下
1 | int bitMask(int highbit, int lowbit) |
这个时候high又变成了0xFFFFFFFF,这有些出乎意料,一时就没什么头绪,不知道有什么问题,于是就开始怀疑编译器是否有编译优化的问题了。
问题解决
经过查找资料,发现右移时右侧操作数大于或等于左侧操作数的位数是属于未定义运算,而编译时确实存在一些编译优化。对于未定义行为,在编译时,分别发生了以下两件事:
- 常量表达式:编译器在编译阶段进行常量求值,依据标准未定义的情况下可能直接优化成 0。
- 变量表达式:运行时生成的移位指令通常只取移位数的低 5 位(对于 32 位整数而言),因而 32 的低 5 位为 0,实际移位相当于“右移 0 位”,结果保持原值 0xFFFFFFFF。
当表达式写为 0xFFFFFFFF >> (highbit+1) 时,虽然 (highbit+1) 计算结果为 32,但由于其为运行时求值的变量表达式,编译器生成的汇编指令遵循硬件实际移位机制。
在许多 CPU 架构(例如 x86)中,用于右移操作的指令(如 SHR 指令)通常只取移位数的低 5 位(对于 32 位操作数),即实际移位数 = 给定移位数 mod 32。
当 (highbit+1) 等于 32 时,其低 5 位为 0,故实际移位操作相当于“右移 0 位”,因此结果仍为原值 0xFFFFFFFF。
这种处理方式与硬件实现有关,虽然在 C 语言标准中两种写法的行为都是未定义的,但实际运行时硬件指令的“模 32”特性使得结果不同。
因此,对于上述问题,一种解决办法就是分开写,即移两次位。
1 | int bitMask(int highbit, int lowbit) |
寄存器保护
问题发现
在某次实验中,根据实验要求,需要使用汇编语言编写其中的部分函数。当然这个实验主要的内容实在优化炫技(划掉),不过在实验中遇到了一个很奇怪的问题。
注意:实验环境为Visual Studio 2022,编译采用x86架,构。其中在生成依赖项时需要对项目进行设置,对项目名称右击后点击生成依赖项下的生成子定义,并勾选masm,这样可以实现汇编和C混合编程。其余均为默认设置。感兴趣的同学可以根据以上内容进行复现。
在实验中需要在Debug版本和Release版本下分别进行程序运行速度的比较,编译阶段并没有问题,Debug版本可以正常运行。但是在Release版本中,当运行即将退出主函数时,抛出了异常。(由于时间较久远,且笔者较懒,没有复现的图片,了解一下原理即可)
问题解决
根据反汇编调试,在return 0处打断点,在查看反汇编后,发现在退出主函数之前进行了一些安全检查,其中一项就是进行寄存器的检查。而在本次场景中,因为Release版本优化程度较高,在程序编译时默认不会使用到寄存器ebx,因而在程序退出时会检查ebx是否进行改动。很遗憾,笔者水平较差,在编写x86汇编的时候用到了ebx并且没有进行寄存器保护,因此由于篡改寄存器导致程序崩溃。
以下给出原始的代码(后期模拟出来的):
1 | .686P |
寄存器保护其实是一个非常简单非常常见的想法,但是由于在编写函数时,笔者原想要减少指令以换取(似乎)更快的速度,同时也是懒得写(划掉,怎么能把最重要的原因写出来),因此并没有做任何寄存器保护的办法。
接下来就简单讲一下寄存器保护思路:
- 对于eax等寄存器,直接压入栈中即可
- 对于ebp,将ebp压入栈中,同时将esp赋值给ebp(即让ebp指向原始栈顶,方便后续esp和ebp等在函数返回时/后恢复原来的值)
- 对于esp,esp减少一部分值,即指向更小的地址,建立一个新栈
而在实验中,由于并没有(直接)使用ebp、esp等寄存器,因而就对这两个不做特殊的保护了,而将其余寄存器全部压入栈中,在函数结束时再弹出。由于懒(划掉),笔者将所有的可能用到的寄存器均压入了栈中,而不是将此函数中用到的寄存器压入栈中(反正结果对了就行了,划掉)。
以下是修改后的代码:
1 | .686P |