GCC是否能生成所有汇编指令的列表?

7

OpenSecurityTraining上托管的Xeno Kovah's Introduction to x86 Assembly第一天的作业中,他分配了以下任务:

现在我们知道的指令(24)

NOP PUSH/POP CALL/RET MOV/LEA ADD/SUB JMP/Jcc CMP/TEST AND/OR/XOR/NOT SHR/SHL IMUL/DIV REP STOS, REP MOV LEAVE

编写一个程序以查找我们未涵盖的指令,并在明天报告该指令。

他进一步说明了这个任务的前提条件:

  • 后面会介绍的指令不算在内:SAL/SAR
  • 跳转的变体或IMUL/DIV变体的MUL/IDIV也不算在内
  • 其他禁用指令:任何浮点指令(因为我们在这门课程中不涉及)
  • 视频中他说你不能使用内联汇编。(当被问到时提到)

与其随意地objdump可执行文件并审核它们,然后创建源代码,是否有可能找到GCC当前输出的x86汇编指令列表?

这个问题的基础似乎是实际上只有很少一部分指令被使用,需要知道这些指令才能进行反向工程(这是本课程的重点)。Xeno似乎正在寻找一种有趣的教学方式来表达这一点。

我认为了解20-30个(不包括变体)就足够了,你将很少需要查阅手册

虽然我欢迎每个人加入我在OpenSecurityTraining的这门课程,但问题是关于我的GCC提出的方法是否可行。不是让人们真正完成Xeno的任务。;)


1
非常有趣的问题! - fuz
1
编写一个程序来查找我们尚未涵盖的指令,这到底是什么意思呢?你不会真的“编写一个程序来查找指令”。他们是指“查找我们尚未涵盖的指令”,并编写一个使用该指令的程序吗?如果是这样,只需用汇编语言编写即可。试图找到一个使用gcc生成此类指令的C程序听起来并不具有生产力。 - lurker
1
@lurker 是的,就是这个意思。使用汇编语言编写程序超出了本次作业的范围。这是一门关注逆向工程的课程,请查看我的更新。 - Evan Carroll
1
“xlat”或“hlt”可能是很少使用的指令。而“aaa”现在也很少见,因为它来自70年代风格的工作。 - Michael Dorgan
不太确定我是否理解正确。如果您想查找所有可能的指令,只需查阅架构手册。例如x86 - llllllllll
显示剩余6条评论
2个回答

5

这个问题的基础似乎是只需了解非常小的一部分指令即可进行逆向工程

是的,一般来说是这样的。有些指令gcc永远不会发出,比如 enter (因为它在现代CPU上比push rbp / mov rbp, rsp / sub rsp, some_constant慢得多)。

其他古老/晦涩的东西,像 xlatloop 也不会被使用因为它们并不更快,而且gcc的-Os并不完全优化大小而不关心性能。(虽然clang -Oz 更加激进,但我不知道是否有人费心教它如何使用loop指令。)

当然,gcc永远不会发出特权指令,比如wrmsr。对于一些非特权指令,例如rdtsccpuid,存在内置函数(__builtin_...函数)。点击此处了解更多信息。
“我能否找到GCC当前输出的x86汇编指令列表?” 这可以通过gcc机器定义文件实现。作为一款便携式编译器,GCC拥有自己的基于文本的机器定义文件语言,用于向编译器描述指令集(每个指令的功能、可使用的寻址模式以及优化器可以最小化的某种“成本”)。请参阅gcc-internals documentation for them
另一种解决这个问题的方法是查看x86指令参考手册(例如this HTML extract,并查看标签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前缀的“字符串”指令。)

3
与其对随机的可执行文件进行objdump并进行审核,然后创建源代码,是否可能找到GCC当前输出的x86汇编指令列表? 您可以查看gcc使用的机器描述文件。在它的源树中,查看gcc/config/i386下的.md文件。x86的核心文件是i386.md;还有其他用于各种扩展到x86的文件(可能包含启发式调整,以在优化不同处理器时使用)。
请注意:这绝对不是一个易读的文件。

我认为了解20-30个(不包括变体)足以让您很少需要检查手册。

“这是非常正确的;在我的逆向工程经验中,99%的代码指令都是相同的;比了解整个x86指令集更有用的是熟悉汇编语言习惯用法,特别是那些经常被编译器发出的。”

话虽如此,从我脑海中想到的一些非常常见的指令缺失(经常被省略且未启用扩展指令集)包括:

  • movzx/movsx 指令
  • inc/dec 操作(在gcc中较少使用,在VC++中很常见
  • neg 指令
  • cdq 指令(idiv之前使用
  • jcxz/jecxz 操作(在gcc中较少使用,在VC++中有些常见)
  • setCC 操作
  • cmpxchg 指令(在同步代码中使用);
  • cmovCC 操作
  • adc 指令(在32位代码中执行64位算术运算时使用)
  • int3 指令(通常在函数边界和一般作为填充物中发出)
  • 其它一些字符串指令(scas/cmps),尤其是在旧编译器上作为预定义序列出现

然后还有整个 SSE & 公司的世界...


gcc不会发出inc(除非使用-Os)。即使对于使用-march=skylake的寄存器目标,它也总是执行add dst,1,这应该告诉它您不关心Silvermont/KNL或Pentium 4(其中inc较慢),但gcc的调优选项并不那么好维护。具有讽刺意味的是,clang在没有调优选项的情况下使用inc,但在使用-march=skylake时使用add reg,1。/facepalm。 - Peter Cordes
我认为gcc永远不会发出jecxz / jrcxz。 它不像loop那么慢,但我认为gcc不知道如何通过分支而不更新标志来优化adc循环。 (通常它只知道如何在__int128(或32位机器上的int64_t)中很好地使用adc,而不是任意精度) - Peter Cordes
我相当确定在调试和反向工程中看到了很多inc,但必须指出的是:(1)它并不总是gcc代码,(2)其中许多可能是lock inc... - Matteo Italia
1
今天我学到了一个新知识,即gcc不会(轻易地)生成inc。尽管我有点怀疑,但在快速搜索中没有找到反例。 - BeeOnRope
@BeeOnRope:GCC现在一般避免在-mtune=generic下使用inc,因为Silvermont系列的原因。如果我没记错的话,它会在-mtune=znver2-mtune=skylake或几乎任何除了KNL或*mont CPU之外的CPU上使用inc/dec。(INC指令与ADD 1:有关系吗?)。现在,主流的Alder Lake有一些Gracemont E核心,这似乎不是一个坏决定,还有低功耗服务器、NAS和低端笔记本电脑。我不知道Gracemont在inc上会有多大的减速。 - Peter Cordes
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接