这个问题的基础似乎是只需了解非常小的一部分指令即可进行逆向工程
是的,一般来说是这样的。有些指令gcc永远不会发出,比如 enter
(因为它在现代CPU上比push rbp
/ mov rbp, rsp
/ sub rsp, some_constant
慢得多)。
其他古老/晦涩的东西,像 xlat
和 loop
也不会被使用因为它们并不更快,而且gcc的-Os
并不完全优化大小而不关心性能。(虽然clang -Oz
更加激进,但我不知道是否有人费心教它如何使用loop
指令。)
当然,gcc永远不会发出特权指令,比如
wrmsr
。对于一些非特权指令,例如
rdtsc
或
cpuid
,存在内置函数(
__builtin_...
函数)。
点击此处了解更多信息。
“我能否找到GCC当前输出的x86汇编指令列表?” 这可以通过gcc机器定义文件实现。作为一款便携式编译器,GCC拥有自己的基于文本的机器定义文件语言,用于向编译器描述指令集(每个指令的功能、可使用的寻址模式以及优化器可以最小化的某种“成本”)。请参阅
gcc-internals documentation for them。
另一种解决这个问题的方法是查看x86指令参考手册(例如
this HTML extract,并查看
x86标签wiki中的其他链接),并寻找您尚未见过的指令。然后编写一个gcc会发现有用的函数。
例如,如果您还没有看到
movsx
(符号扩展),那么就编写一个相关函数。
long long foo(int x) { return x; }
而 gcc -O3 将会生成 (从 Godbolt 编译器浏览器)
movsx rax, edi
ret
或者,为了在rax
中进行符号扩展,获取cdqe
(也称AT&T语法中的cltq
),强制gcc在符号扩展之前进行数学运算,这样它可以先在eax
中生成结果(使用复制和添加的lea
)。
long long bar(unsigned x) { return (int)(x+1)
lea eax, [rdi+1]
cdqe
ret
# clang chooses inc edi / movsxd rax, edi
请参阅
Matt Godbolt在CppCon2017的演讲: "最近我的编译器为我做了什么?揭开编译器的盖子" ,以及
如何从GCC/clang汇编输出中去除“噪音”?。
让gcc发出旋转指令很有趣。
C ++中循环移位(旋转)操作的最佳实践。您将其编写为gcc可以识别为旋转的移位/OR。
因为C语言没有提供标准函数来完成现代CPU可以执行的许多任务(旋转,popcnt,计算前导/尾随零),唯一的便携式方法是编写等效函数并使编译器识别该模式。当使用
-mpopcnt
编译时(例如启用
-march = haswell
),gcc和clang可以将整个循环优化为单个
popcnt
指令,如果你很幸运。如果不是,则会得到一个非常缓慢的循环。可靠的非便携式方法是使用
__builtin_popcount()
,如果目标支持它,则将其编译为
popcnt
指令,否则进行表查找。
_mm_popcnt_u64
是
popcnt
或无:如果目标不支持该指令,则不会编译它。
当然,这种方法的困境是:只有你已经了解x86指令集并且知道何时使用某个指令才能使优化编译器起到作用!
(而gcc选择做什么,例如在某些情况下内联字符串比较到
rep cmpsb
,尽管我不确定这是否是最优选择。只有
rep movs
/
rep stos
在现代CPU上支持“快速字符串”。但我认为gcc永远不会使用
lods
或任何没有
rep
前缀的“字符串”指令。)
gcc
生成此类指令的C程序听起来并不具有生产力。 - lurker