如何从汇编指令到 C 代码
How to go From Assembler instruction to C code
我有一个任务,除其他外,我需要在 .asm 文件中查找特定指令,然后 "reverse engineer"(找出)C 代码的哪一部分导致它被执行在汇编程序级别。
(例子在正文下方)
最快(最简单)的方法是什么?或者更确切地说,我注意到 .asm 文件中它周围的其他命令/指令/标签 should/could,它们会引导我找到正确的 C 代码?
我对汇编代码的经验几乎为零,很难弄清楚具体是哪几行 C 代码导致特定指令发生。
架构,如果有什么区别的话,就是 TriCore。
示例:
通过跟踪使用变量的位置,我设法找出是什么 C 代码导致在 asm 文件中插入
.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;
I'm supposed to patch an existing Instruction Set Test, that doesn't test all the used instructions. So I need to look at the asm file of one level of code and find out what C code causes the instruction to happen so I can use it in my patch.
你的目标很疯狂,你的问题的前半部分是倒退的/与你的实际问题只有松散的关系。
可能有一种方法可以说服您的编译器使用您希望它使用的每条特定指令,但它将特定于您的编译器版本、选项和所有周围代码,包括 header 中的潜在常量文件。
如果你想测试 ISA 中的所有指令,希望你能说服 C 编译器以某种方式生成它们是完全错误的方法。你希望你的测试在未来继续测试同样的东西,所以你应该。 如果你需要特定的asm,写成asm。
这与几周前针对 ARM 提出的相同问题:How to force IAR to use desired Cortex-M0+ instructions (optimization will be disabled for this func.) 除了您说您将在启用优化的情况下构建(这可能更容易生成更广泛的指令: 有些可能仅用作简单法线的窥孔优化 code-gen).
此外,从 asm 开始并将其反转为等效的 C 并不能保证编译器在编译时会选择该指令,因此问题标题与您的实际问题只是松散相关。
如果您仍然希望 hand-hold 编译器生成特定的 asm,以创建可能只能使用非常特定的编译器执行您想要的操作的脆弱源代码/版本/选项,第一步是思考“这条指令什么时候会成为优化做事方式的一部分?”。
通常这种思路对于通过调整源代码以更有效地编译来进行优化更有用。首先,您要考虑您正在编写的函数的高效 asm 实现。然后以相同的方式编写 C 或 C++ 源代码,即使用您希望编译器使用的相同临时文件。例如,请参阅 What is the efficient way to count set bits at a position or lower?,其中我能够 hand-hold gcc 使用更有效的指令序列,就像 clang 在我第一次尝试时所做的那样。
有时这可以很好地工作;出于您的目的,当 instruction-set 只有一种真正好的方法来做某事时,这很简单。例如ld.bu
看起来像一个 byte-load,带有零扩展(u
表示无符号)到一个完整的寄存器中。 unsigned foo(unsigned char*p) {return *p;}
应该编译成那个,你可以使用 noinline
属性来阻止它优化。
但是 insert
,如果那是将 zero-bit 插入到位域中,那么假设 TriCore 有 and
和 ~1
(0xFE) and-immediate。如果 insert
有一个 non-immediate 形式,这可能是 single-bit bitfield = rand()
最有效的选择(或者任何在使用 constant-propagation 优化后仍然不是 compile-time 常量的值) ).
对于 TriCores 的压缩算术 (SIMD) 指令,您将需要编译器 auto-vectorize,或使用内部指令。
ISA 中可能有一些指令是您的编译器永远不会发出的。尽管我认为您只是想测试编译器在您代码的其他部分发出的指令?你说"all the used instructions",而不是"all the instructions",这样至少保证任务是可以的
带有 arg 的 non-inline 函数是为 run-time 变量强制 code-gen 的绝佳方式。那些查看 compiler-generated asm 的用户经常编写带有 args 和 return 值(或存储到全局或 volatile
)的小函数来强制编译为没有的东西生成代码丢弃结果,并且没有 constant-propagation 将整个函数变成 return 42;
,即 mov
-immediate / ret
。有关阅读 compiler-generated asm 的一些很棒的初学者介绍,请参阅 How to remove "noise" from GCC/clang assembly output? for more about that, and also Matt Godbolt's CppCon2017 talk: “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”,以及现代优化编译器为小函数做了哪些事情。
分配给 volatile
然后读取该变量将是击败 constant-propagation 的另一种方法,即使对于需要 运行 没有外部输入的测试,如果这比使用更容易的话非内联函数。 (对于在 C 源代码中读取的每个单独时间,编译器从 volatile
中获得 re-load,即他们必须假设它可以被异步修改。)
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
}
[...] It's optimized
您显示的编译器输出看起来肯定没有优化。看起来加载/设置位/存储,然后加载/清除位/存储,应该优化为仅加载/清除位/存储。除非这些 asm 块不是真正连续的,并且您显示的是来自粘贴在一起的两个不同块的代码。
此外,InsertStruct.SomeMember = 0x0u;
是一个不完整的描述:它显然取决于结构定义;我假设您使用了 int SomeMember :1;
single-bit 位域成员?根据此 TriCore ISA ref manual I found,insert
将一系列位从一个寄存器复制到另一个寄存器,在指定的插入位置,并以寄存器和直接源形式出现。
替换整个字节可能只是一个存储而不是 read/modify/write。所以这里的关键是结构定义,而不仅仅是编译成指令的语句。
The architecture is TriCore (if that makes any difference).
当然可以。汇编代码始终是特定于体系结构的。
... what part of the C code causes it to be executed on an assembler level.
使用高度优化的编译器时,您几乎没有机会:
例如,TriCore 的任务编译器有时甚至会为两个不同的 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 代码。
不幸的是,这需要 很多 时间 - 但我不知道有什么更快的方法。
我有一个任务,除其他外,我需要在 .asm 文件中查找特定指令,然后 "reverse engineer"(找出)C 代码的哪一部分导致它被执行在汇编程序级别。 (例子在正文下方)
最快(最简单)的方法是什么?或者更确切地说,我注意到 .asm 文件中它周围的其他命令/指令/标签 should/could,它们会引导我找到正确的 C 代码?
我对汇编代码的经验几乎为零,很难弄清楚具体是哪几行 C 代码导致特定指令发生。
架构,如果有什么区别的话,就是 TriCore。
示例: 通过跟踪使用变量的位置,我设法找出是什么 C 代码导致在 asm 文件中插入
.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;
I'm supposed to patch an existing Instruction Set Test, that doesn't test all the used instructions. So I need to look at the asm file of one level of code and find out what C code causes the instruction to happen so I can use it in my patch.
你的目标很疯狂,你的问题的前半部分是倒退的/与你的实际问题只有松散的关系。
可能有一种方法可以说服您的编译器使用您希望它使用的每条特定指令,但它将特定于您的编译器版本、选项和所有周围代码,包括 header 中的潜在常量文件。
如果你想测试 ISA 中的所有指令,希望你能说服 C 编译器以某种方式生成它们是完全错误的方法。你希望你的测试在未来继续测试同样的东西,所以你应该。 如果你需要特定的asm,写成asm。
这与几周前针对 ARM 提出的相同问题:How to force IAR to use desired Cortex-M0+ instructions (optimization will be disabled for this func.) 除了您说您将在启用优化的情况下构建(这可能更容易生成更广泛的指令: 有些可能仅用作简单法线的窥孔优化 code-gen).
此外,从 asm 开始并将其反转为等效的 C 并不能保证编译器在编译时会选择该指令,因此问题标题与您的实际问题只是松散相关。
如果您仍然希望 hand-hold 编译器生成特定的 asm,以创建可能只能使用非常特定的编译器执行您想要的操作的脆弱源代码/版本/选项,第一步是思考“这条指令什么时候会成为优化做事方式的一部分?”。
通常这种思路对于通过调整源代码以更有效地编译来进行优化更有用。首先,您要考虑您正在编写的函数的高效 asm 实现。然后以相同的方式编写 C 或 C++ 源代码,即使用您希望编译器使用的相同临时文件。例如,请参阅 What is the efficient way to count set bits at a position or lower?,其中我能够 hand-hold gcc 使用更有效的指令序列,就像 clang 在我第一次尝试时所做的那样。
有时这可以很好地工作;出于您的目的,当 instruction-set 只有一种真正好的方法来做某事时,这很简单。例如ld.bu
看起来像一个 byte-load,带有零扩展(u
表示无符号)到一个完整的寄存器中。 unsigned foo(unsigned char*p) {return *p;}
应该编译成那个,你可以使用 noinline
属性来阻止它优化。
但是 insert
,如果那是将 zero-bit 插入到位域中,那么假设 TriCore 有 and
和 ~1
(0xFE) and-immediate。如果 insert
有一个 non-immediate 形式,这可能是 single-bit bitfield = rand()
最有效的选择(或者任何在使用 constant-propagation 优化后仍然不是 compile-time 常量的值) ).
对于 TriCores 的压缩算术 (SIMD) 指令,您将需要编译器 auto-vectorize,或使用内部指令。
ISA 中可能有一些指令是您的编译器永远不会发出的。尽管我认为您只是想测试编译器在您代码的其他部分发出的指令?你说"all the used instructions",而不是"all the instructions",这样至少保证任务是可以的
带有 arg 的 non-inline 函数是为 run-time 变量强制 code-gen 的绝佳方式。那些查看 compiler-generated asm 的用户经常编写带有 args 和 return 值(或存储到全局或 volatile
)的小函数来强制编译为没有的东西生成代码丢弃结果,并且没有 constant-propagation 将整个函数变成 return 42;
,即 mov
-immediate / ret
。有关阅读 compiler-generated asm 的一些很棒的初学者介绍,请参阅 How to remove "noise" from GCC/clang assembly output? for more about that, and also Matt Godbolt's CppCon2017 talk: “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”,以及现代优化编译器为小函数做了哪些事情。
分配给 volatile
然后读取该变量将是击败 constant-propagation 的另一种方法,即使对于需要 运行 没有外部输入的测试,如果这比使用更容易的话非内联函数。 (对于在 C 源代码中读取的每个单独时间,编译器从 volatile
中获得 re-load,即他们必须假设它可以被异步修改。)
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
}
[...] It's optimized
您显示的编译器输出看起来肯定没有优化。看起来加载/设置位/存储,然后加载/清除位/存储,应该优化为仅加载/清除位/存储。除非这些 asm 块不是真正连续的,并且您显示的是来自粘贴在一起的两个不同块的代码。
此外,InsertStruct.SomeMember = 0x0u;
是一个不完整的描述:它显然取决于结构定义;我假设您使用了 int SomeMember :1;
single-bit 位域成员?根据此 TriCore ISA ref manual I found,insert
将一系列位从一个寄存器复制到另一个寄存器,在指定的插入位置,并以寄存器和直接源形式出现。
替换整个字节可能只是一个存储而不是 read/modify/write。所以这里的关键是结构定义,而不仅仅是编译成指令的语句。
The architecture is TriCore (if that makes any difference).
当然可以。汇编代码始终是特定于体系结构的。
... what part of the C code causes it to be executed on an assembler level.
使用高度优化的编译器时,您几乎没有机会:
例如,TriCore 的任务编译器有时甚至会为两个不同的 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 代码。
不幸的是,这需要 很多 时间 - 但我不知道有什么更快的方法。