如何将汇编指令转换为C代码

5
我有一个任务,需要查看.asm文件以找到特定指令并“反向工程”(查明)在汇编层面上执行它的C代码的哪一部分。 (以下是文本示例)
有什么最快(最简单)的方法可以做到这一点?或者更好地说,在.asm文件中周围的其他命令/指令/标签应该/可以引起我注意,以指导我找到正确的C代码?
我几乎没有使用汇编代码的经验,很难确定哪些确切的C代码行使某个特定指令发生。
架构,如果有任何区别,就是TriCore。
示例: 通过跟踪变量的使用情况,我成功地找到了在asm文件中导致插入的C代码。
 .L23:
    movh.a  a15,#@his(InsertStruct)
    ld.bu   d15,[a15]@los(InsertStruct)
    or  d15,#1
    st.b    [a15]@los(InsertStruct),d15
.L51:
    ld.bu   d15,[a15]@los(InsertStruct)
    insert  d15,d15,#0,#0,#1
    st.b    [a15]@los(InsertStruct),d15
.L17:
    mov d15,#-1

这让我想到了以下的C代码:

InsertStruct.SomeMember = 0x1u;

InsertStruct.SomeMember = 0x0u;

1
这个想法是让你展示对C语言、编译器和汇编器的理解。有一些工具可以帮助你完成这个任务,有时被称为“反汇编器”。然而,在大多数情况下,结果令人失望,有时甚至比汇编代码更难以理解,即使对于经验丰富的汇编程序员也是如此。 - Yunnosch
4
你应该认真完成这个练习,而不是回避它。这个练习的目的是提高你缺失的汇编技能和理解并重新实现算法的必不可少的能力。因此,你应该关注每一行代码,并且最快的方法就是理解代码的含义。 - Margaret Bloom
2
“哪些C代码行导致特定指令的发生。”-整个源代码导致整个汇编程序的发生。在优化的代码中,没有直接的行到行映射,有时优化器会大幅改变算法,例如通过乘法进行x / 15,或通过直接计算结果来删除整个循环求和值等等...如果您试图从这样的汇编重构C源代码,则最终将得到完全不同的源代码(算法方面)。 - Ped7g
2
谢谢你的回答,但是很遗憾这并没有帮到我,我明白这是一个非常笼统的问题。 不幸的是,这不是一个练习,而是工作。 我应该修补一个现有的指令集测试,但它并没有测试所有使用的指令。所以我需要查看一个级别的汇编文件,找出哪段C代码导致了这个指令的发生,这样我就可以在我的修补程序中使用它。@ Lundin,是的,考虑到Ped7g所说的,这确实很难阅读。它经过了优化,一行C代码并不能确定某个特定的指令是否被使用。"相互作用"才是关键。 - vandelfi
4
如果你想测试一个ISA中的所有指令,希望通过说服C编译器以某种方式生成它们来实现这一点,完全是错误的方法。下一个版本的编译器或更改某个常量可能会导致不同的代码生成。如果你需要特定的汇编语言,就用汇编语言编写。 - Peter Cordes
显示剩余6条评论
2个回答

3
我需要修补一个现有的指令集测试,因为它没有测试所有使用的指令。所以我需要查看一个代码级别的汇编文件,找出导致指令发生的C代码,以便在我的补丁中使用它。
你的目标是疯狂的,并且你问题的前半部分是反向的/只与你真正的问题松散相关。可能有一种方法可以说服编译器使用你想要的每个特定指令,但这将是特定于你的编译器版本、选项和所有周围的代码,包括头文件中的常量。如果你想测试一个ISA中的所有指令,希望能够以某种方式说服C编译器生成它们,那么完全是错误的方法。你希望你的测试在未来继续测试相同的东西,所以你应该写汇编代码,如果你需要特定的汇编语言。

这是几周前针对ARM提出的相同问题:如何强制IAR使用期望的Cortex-M0+指令(将禁用此函数的优化),只不过您说您将启用优化构建(这可能会使得生成更广泛的指令更容易:有些只能用作简单常规代码生成的窥孔优化)。


此外,从汇编语言开始并将其反向转换为等效的 C 代码,并不能保证编译器在编译时选择该指令,因此问题标题只与您实际问题 loosly 相关。
如果你仍然想要手动编译生成特定的汇编代码,创建脆弱的源代码,只能在特定的编译器/版本/选项下实现你想要的功能,那么第一步就是思考“这条指令何时会成为优化方式的一部分?”。

通常,这种思路更适用于通过调整源代码来进行优化。首先,你需要考虑一个函数的高效汇编实现。然后,你可以使用相同的临时变量编写C或C++源代码,希望编译器也会使用这些变量。例如,请参见如何高效计算比特位,我能够手动引导gcc使用更高效的指令序列,就像clang对我的第一次尝试所做的那样。

有时候这种方法很有效;当指令集只有一种非常好的做法时,对于您的目的来说,它是简单的。例如,ld.bu看起来像是将一个字节加载到一个完整寄存器中并进行零扩展(u表示无符号)。unsigned foo(unsigned char*p) {return *p;}应该编译成这样,您可以使用noinline属性防止其被优化掉。

但是,如果insert是将零位插入到位域中,则同样可以使用~1(0xFE)进行and,假设TriCore具有与立即数的与操作。如果insert有一个非立即数形式,则对于single-bit bitfield = rand()(或任何在常量传播优化后仍不是编译时常量的值),这可能是最有效的选项。

对于TriCores的打包算术(SIMD)指令,您需要编译器自动矢量化或使用内部函数。

在ISA中可能会有一些指令,你的编译器永远不会发出。尽管我认为你只是想测试编译器在代码其他部分发出的指令?你说“所有使用的指令”,而不是“所有指令”,所以这至少保证了任务是可能的。


一种带有参数的非内联函数是强制运行时变量代码生成的绝佳方式。我们经常查看编译器生成的汇编代码并编写小函数来使用参数返回值(或存储到全局或volatile),以便在不丢弃结果或常量传播将整个函数转换为return 42;(即mov-immediate / ret)的情况下,强制编译器为某些内容生成代码。请参见如何从GCC/clang汇编输出中去除“噪音”?了解更多信息,并查看Matt Godbolt的CppCon2017演讲:“我的编译器最近为我做了什么?打开编译器的盖子”,其中介绍了阅读编译器生成的汇编代码的初学者入门知识以及现代优化编译器为小函数执行的操作类型。

如果使用volatile赋值并读取该变量,即使测试需要在没有外部输入的情况下运行,这也是打败常数传播的另一种方法,如果使用不带内联函数比较容易。 (编译器必须从C源中每次单独读取volatile,即它们必须假设它可能被异步修改。)

int main(void) {
    volatile int vtmp = 123;
    int my_parameter = vtmp;

    ... then use my_parameter, not vtmp, so CSE and other optimizations can still work
 }

[...] 它已经被优化了。

您展示的编译器输出看起来并不是经过优化的。它看起来像是加载/设置位/存储,然后加载/清除位/存储,这应该优化为只加载/清除位/存储。除非这些汇编块不是真正连续的,并且您正在显示从两个不同的块粘贴在一起的代码。

此外,InsertStruct.SomeMember = 0x0u; 是一个不完整的描述:它显然取决于结构体定义;我假设您使用了一个 int SomeMember :1; 单比特位域成员?根据我找到的 TriCore ISA参考手册insert 将一个寄存器中的一系列位复制到另一个寄存器中,在指定的插入位置处,并以寄存器和立即源形式存在。

替换整个字节可能只需要进行存储而不是读取/修改/写入。因此,关键在于结构体定义,而不仅仅是编译为指令的语句。


2
@vandelfi:我建议你雇用一位了解汇编语言的顾问来查看你首先要解决的问题,并帮助你确定是否采用正确的方法。你是想测试不同TriCore芯片的硬件兼容性吗?如果是这样,手写一些汇编代码会更有意义。 - Peter Cordes
通过@vandelfi或者其他方式,你可以在一个类库中编写具有合理API的函数,然后编写单元测试应用程序来调用这些函数并验证结果,而不必关心使用的特定指令。不幸的是,你目前的问题听起来像问题XY - Ped7g
1
@vandelfi 如果你想要专门编写用于测试CPU故障的特定汇编代码。虽然我很难想象CPU仅因为特定指令而发生故障,在我天真的观点中(我不是硬件人员,对此了解甚少),故障的CPU可能会有芯片的某个部分损坏,这将导致各种副作用,例如测试的设置/拆卸已经受到严重影响,被测试指令的结果可能也会有偏差,但问题是是否还有足够的寿命来报告它。 - Ped7g
1
@vandelfi,仅测试指令将无法捕获特定地址/数据线路的故障,即测试可能会变绿色,因为它们将使用仍然可访问的RAM部分,而主代码已经失控,因为其所需的RAM部分的地址线路将出现故障。您的测试代码不仅应运行与主代码相同的指令,还应验证正在由实时代码积极使用的内存和其他资源,即将实时数据与一些备用空间交错,或在测试期间具有访问锁并停止主代码。 - Ped7g
1
彼得:你知道什么才是终极笑话吗?如果他们无法使用汇编语言创建那个测试,因为由于某些标准需要使用C语言,并且asm源代码将无法通过某些策略检查,而经过大量运气修补的摇摇欲坠的C编译器将被认为是正确的解决方案。 :D - Ped7g
显示剩余6条评论

3
架构是TriCore(如果有区别的话)。
当然。汇编代码总是与架构相关的。
当使用高度优化的编译器时,你几乎没有机会:
例如,TriCore的Tasking编译器有时甚至为两个不同的C文件中的两个不同的C代码行生成一个汇编代码片段(仅存储一次!)。
但是,在您的示例代码中,代码未经过优化(除非您命名为InsertStruct的结构是volatile)。
在这种情况下,您可以打开调试信息编译代码并提取调试信息:从ELF格式文件中,您可以使用像addr2line(GNU编译器套件的免费软件)这样的工具来检查哪行C代码对应于某个地址的指令。
(注意:addr2line工具是与体系结构无关的,只要两个体系结构具有相同的宽度(32位)、相同的字节序并且都使用ELF文件格式;您可以使用addr2line针对ARM从TriCore文件获取信息。)
如果您真的必须理解一段汇编代码片段,我自己通常会执行以下操作:
我启动一个文本编辑器并粘贴汇编代码:
movh.a  a15,#@his(InsertStruct)
ld.bu   d15,[a15]@los(InsertStruct)
or      d15,#1
st.b    [a15]@los(InsertStruct),d15
...

然后我用伪代码替换每个指令:

a15 =  ((((unsigned)&InsertStruct)>>16)<<16;
d15 =  *(unsigned char *)(a15 + (((unsigned)&InsertStruct)&0xFFFF));
d15 |= 1;
*(unsigned char *)(a15 + (((unsigned)&InsertStruct)&0xFFFF)) = d15;
...

在下一步中,我尝试简化这段代码:
a15 =  ((unsigned)&InsertStruct) & 0xFFFF0000;

然后:

d15 = *(unsigned char *)((((unsigned)&InsertStruct) & 0xFFFF0000) + (((unsigned)&InsertStruct)&0xFFFF));
...

然后:

d15 = *(unsigned char *)((unsigned)&InsertStruct);
...

然后:

d15 = *(unsigned char *)&InsertStruct;
...

最终,我尝试替换跳转指令:
d15 = 0;
if(d14 == d13) goto L123;
d15 = 1;
L123:

...变成:

d15 = 0;
if(d14 != d13) d15 = 1;

...最后(也许):

d15 = (d14 != d13);

最终,在文本编辑器中获得了C代码。

不幸的是,这需要很多时间 - 但我不知道任何更快的方法。


你有点过于复杂化a15的东西了。你并不认为那些指令实际上是用C运算符来移动地址吧?我的意思是,unsigned char *a15 = hi(&InsertStruct) / unsigned d15 = a15[lo(InsertStruct)]。或者直接使用更合理的C语言,比如d15 = InsertStruct.SomeMember;,一旦你理解编译器如何处理具有固定宽度指令的静态地址常量,你就会这样考虑它。 - Peter Cordes
@ Martin Rosenau 感谢您抽出时间来回答我的问题。我接受这个答案,因为您提供了两种我可以用来解决问题的方法。 虽然没有汇编语言的先前知识,但比没有好。再次感谢。 - vandelfi

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