How to go From Assembler instruction to C code
Asked Answered
S

2

5

I have an assignment where, among other things, I need to look in an .asm file to find a certain instruction and "reverse engineer" (find out) what part of the C code causes it to be executed on an assembler level. (Example below the text)

What would be the fastest (easiest) way to do this. Or better to say, what other commands / instructions / labels that are around it in the .asm file should/could I pay attention to, that would guide me to the right C code?

I have next to zero experience with assembler code and it is tough to figure out what exact lines of C code cause a particular instruction to happen.

The architecture, if that makes any difference, is TriCore.

Example: I managed to figure out what C code causes an insert in the asm file, by following where the variables are used

 .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

that led me to the following C code:

InsertStruct.SomeMember = 0x1u;

InsertStruct.SomeMember = 0x0u;
Solubility answered 18/12, 2017 at 10:8 Comment(11)
The idea is that you demonstrate understanding of C, compilers and assemblers. There are tools which might be able to do this for you, sometimes called "disassembler". The result is however disappointing in most cases and sometimes is less understandable than the assembler code is for a seasoned assembler programmer.Epochal
You should work through the exercise, not around it. The aim of it is to improve your (missing) assembly skills and the indispensable ability to understand and re-implement an algorithm. So: you should pay attention to every line, and the fastest way is understanding it.Helices
"what exact lines of C code cause a particular instruction to happen." - the exact whole source causes whole assembly to happen. There's no direct line to line mapping, at least in optimized code, sometimes the optimizer will change the algorithm considerably, like doing x / 15 by multiplication, or removing whole loop summing values by calculating the result directly, etc... If you would try to reconstruct C source from such assembly, you would end with completely different source (algorithm-wise).Claudiaclaudian
@MargaretBloom Reading compiler-generated assembler is not necessarily a good way to learn. Such code is often very strange and hard to read.Still
Thank you for answering, but this unfortunately doesn't help and I get that it's a very general question. Unfortunately it's not an exercise, but work. 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. @ Lundin , yes it's very hard to read considering what Ped7g said. It's optimized and one line of C code doesn't mean a certain instruction is used. The "interplay" so to say is the key.Solubility
If you want to test all the instructions in an ISA, hoping that you can convince a C compiler to generate them somehow is totally the wrong approach. The next version of the compiler, or changing a constant somewhere, could lead to different code-gen. If you need specific asm, write in asm.Shippy
@MargaretBloom I doubt anyone would really use TriCore as architecture for an exercise.Bradleybradly
@Yunnosch: You mean "decompiler". Disassembler is machine code -> asm text. reverseengineering.stackexchange.com/questions/tagged/…Shippy
@PeterCordes I probably confused it, or somebody using the term differently...Epochal
@MartinRosenau It ought to be or I'll never feel safe again in a car :)Helices
@MargaretBloom wait, you still feel safe around all those IoT and autonomous things creeping up? :D wow. For me just sticking "smart" sticker on them was never reassuring enough, I mean they are certainly way smarter than humans in certain arithmetical way, but that plays well together with real life only in about ~99.9% cases. :)Claudiaclaudian
B
3

The architecture is TriCore (if that makes any difference).

Of course. Assembler code is always architecture specific.

... what part of the C code causes it to be executed on an assembler level.

When using a highly optimizing compiler you nearly have no chance:

The Tasking compiler for TriCore for example sometimes even generates one fragment of assembly code (stored only once in memory!) for two different lines of C code in two different C files!

However the code in your example is not optimized (unless the structure you named InsertStruct is volatile).

In this case you could compile your code with debugging information switched on and extract the debugging information: From an ELF format file you can use tools like addr2line (freeware from the GNU compiler suite) to check which line of C code corresponds to an instruction at a certain address.

(Note: The addr2line tool is architecture independent as long as both architectures have same width (32-bit), the same endianness and both use the ELF file format; you could use addr2line for ARM to get the information from a TriCore file.)

If you really have to understand a fragment of assembler code I myself typically do the following:

I start a text editor and paste in the assembler code:

movh.a  a15,#@his(InsertStruct)
ld.bu   d15,[a15]@los(InsertStruct)
or      d15,#1
st.b    [a15]@los(InsertStruct),d15
...

Then I replace each instruction by the pseudo-code equivalent:

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

In the next step I try to simplify this code:

a15 =  ((unsigned)&InsertStruct) & 0xFFFF0000;

Then:

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

Then:

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

Then:

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

In the end I try to replace jump instructions:

d15 = 0;
if(d14 == d13) goto L123;
d15 = 1;
L123:

... becomes:

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

... and finally (maybe):

d15 = (d14 != d13);

In the end you have C code in the text editor.

Unfortunately this takes much time - but I don't know any faster method.

Bradleybradly answered 18/12, 2017 at 11:58 Comment(2)
You're somewhat overcomplicating the stuff with a15. You don't really think of those instructions as actually shifting around the address with C operators, do you? I mean, unsigned char *a15 = hi(&InsertStruct) / unsigned d15 = a15[lo(InsertStruct)]. Or just go directly to more reasonable C like d15 = InsertStruct.SomeMember; which is how you'd think of it once you understand how compilers deal with static address constants on machines with fixed-width instructions.Shippy
@ Martin Rosenau Thank you for taking the time to answer. I'm accepting this as the answer, considering you provided two methods I could possibly use to overcome my problem. I doub't it will help much without previous knowledge of assembler, but it's better than nothing. Thanks again.Solubility
S
3

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.

Your goal is insane, and the first half of your question is backwards / only loosely related to your real problem.

There might be a way to convince your compiler to use each specific instruction you want it to, but it will be specific to your compiler version, options, and all the surrounding code including potentially constants in header files.

If you want to test all the instructions in an ISA, hoping that you can convince a C compiler to generate them somehow is totally the wrong approach. You want your test to keep testing the same thing in the future, so you should . If you need specific asm, write in asm.

This is the same question asked a couple weeks ago for ARM: How to force IAR to use desired Cortex-M0+ instructions (optimization will be disabled for this func.) except that you say you're going to build with optimization enabled (which may make it easier to get a wider range of instruction generated: some may only be used as peephole optimizations over the simple normal code-gen).


Also, starting with asm and reversing that into equivalent C is no guarantee that the compiler will choose that instruction when compiling, so the question title is only loosely related to your real problem.


If you do still want to hand-hold a compiler into generating specific asm, to create brittle source code that may only do what you want with very specific compiler / version / options, the first step would be to think "when would this instruction be part of an optimized way of doing something?".

Usually this line of thinking is more useful for optimizing by tweaking the source to compile more efficiently. First you think about an efficient asm implementation of function you're writing. Then you write your C or C++ source the same way, i.e. using the same temporaries you hope the compiler will use. For an example, see What is the efficient way to count set bits at a position or lower? where I was able to hand-hold gcc into using a more efficient sequence of instructions, like clang was doing for my first attempt.

Sometimes this can work well; for your purposes it's simple when the instruction-set only has one really good way to do something. e.g. ld.bu looks like a byte-load with zero extension (u for unsigned) into a full register. unsigned foo(unsigned char*p) {return *p;} should compile to that, and you can use a noinline attribute to stop it from optimizing away.

But insert, if that's inserting a zero-bit into a bitfield, could just as easily have been an and with ~1 (0xFE), assuming TriCore has and-immediate. If insert has a non-immediate form, that is probably the most efficient option for single-bit bitfield = rand() (or any value that's still not a compile-time constant after optimization with constant-propagation).

For TriCores' packed arithmetic (SIMD) instructions, you're going to need the compiler to auto-vectorize, or use an intrinsic.

There might well be some instructions in the ISA that your compiler will never emit. Although I think you're only trying to test the instructions that the compiler does emit in other parts of your code? You say "all the used instructions", not "all the instructions", so that at least guarantees that the task is possible.


A non-inline function with an arg is an excellent way to force code-gen for run-time variables. Those of use who look at compiler-generated asm frequently write small functions that take args and return a value (or store to a global or volatile) to force the compile to generate code for something without discarding the result, and without constant-propagation turning the whole function into return 42;, i.e. a mov-immediate / ret. See 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” for some great beginner intro to reading compiler-generated asm, and what kind of stuff modern optimizing compilers do for small functions.

Assigning to a volatile and then reading that variable would be another way to defeat constant-propagation even for a test that needs to run without external inputs, if that's easier than using noinline functions. (Compilers have re-load from a volatile for every separate time it's read in the C source, i.e. they have to assume it can be asynchronously modified.)

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

The compiler output you show definitely doesn't look optimized. It looks like load / set a bit / store, then load / clear a bit / store, which should have optimized down to just load / clear the bit / store. Unless those asm blocks weren't really contiguous, and you're showing code from two different blocks pasted together.

Also, InsertStruct.SomeMember = 0x0u; is an incomplete description: it obviously depends on the struct definition; I assume you used an int SomeMember :1; single-bit bitfield member? According to this TriCore ISA ref manual I found, insert copies a range of bits from one register to another, at a specified insert position, and comes in register and immediate source form.

Replacing a whole byte could just be a store instead of a read/modify/write. So the key here is the struct definition, not just the statement that compiled to the instruction.

Shippy answered 18/12, 2017 at 11:53 Comment(11)
ok, thank you for the detailed answer. You are right about the struct. I cannot paste the original code here, so I just pasted what i did to recreate the insert instruction and the asm output that confirmed to me that an insert was used. You are also right that the Instruction set test is supposed to test only a part of the whole instruction test, i.e. the instructions that are used in one level of code, and the part of the code that is supposed to test them is was spliced onto this from a different project so I'm supposed to patch it up by testing around 30 more instructions used.Solubility
It seems to me I'm missing some serious assembler knowledge to finish this. Most of what you have explained is sadly above my current skill level. But thank you for taking the time to lay down all the finer points.Solubility
@vandelfi: I'd recommend you hire a consultant that knows asm to come and look at what problem you're trying to solve in the first place with this test, and help you figure out whether it's even the right approach. Are you trying to test hardware compatibility with different TriCore chips or something? If so, just writing some asm by hand would make so much more sense.Shippy
@Solubility or the other way around, having your functions with reasonable API inside kind-of-library you can produce unit testing app calling you functions and verifying results, not caring about particular instructions used at all. Your question at the moment unfortunately sounds like Problem XY.Claudiaclaudian
so a safety concept is in question in the automotive field. We have the code responsible for functionality and then code that monitors that, and then some more code that tests if the instructions used in the monitoring part are manipulating the data properly. that's what I mean with Instruction set test.Solubility
@vandelfi: what kind of problems are you worried about? Do you have a "threat model"? Are you worried about the compiler using certain instructions incorrectly, or about the hardware incorrectly executing certain instructions? Or both? If bad code-gen is a worry, then simply testing different code that uses the same instruction a different way in a different context isn't a great test. I think Ped7g's unit-test suggestion is good: try to write tests that use the same actual compiled functions you care about, not other code compiled differently.Shippy
the IST part of code is running constantly and testing if the data calculated by higher level functions (that use certain asm instructions) has resulted in an expected result. Otherwise a fault reaction occurs. It's a run time thing, has to do with things going wrong on the hardware.Solubility
@Solubility then you want specialized assembly code written directly with purpose to test CPU malfunctioning. Although I have very hard time to imagine a CPU malfunctioning only for particular instruction, in my naive opinion (I'm not HW guy and I know very little about it) a malfunctioning CPU will probably have certain part of chip bad, which will cause all kind of side effects, like the test setup/teardown will be already heavily affected, the result of the tested instruction may be skewed too, but the question is whether there will be enough life to report it.Claudiaclaudian
@Solubility also testing just instructions will not catch malfunction in particular address/data line, i.e. the tests may go green as they will use part of RAM which is still accessible, while the main code will be already going insane, as the address line needed for it's portion of RAM will be malfunctioning. Your test code should then not only run same instructions as your main code, but also validate the memory and other resources being actively used by live code, i.e. either interleave live data with some spare space for tests, or have access locks and stop the main code during test.Claudiaclaudian
Peter: you know what would be ultimate joke? If they would be unable to create that test in assembly, because C is required due to some standard and asm source would not go through some policy check, while the shaky duct-tape patched C compiled with lot of luck to certain instructions will be considered as correct solution. :DClaudiaclaudian
Thank you for all for your comments. I wish that I could have found a better way to describe the whole predicament.Solubility
B
3

The architecture is TriCore (if that makes any difference).

Of course. Assembler code is always architecture specific.

... what part of the C code causes it to be executed on an assembler level.

When using a highly optimizing compiler you nearly have no chance:

The Tasking compiler for TriCore for example sometimes even generates one fragment of assembly code (stored only once in memory!) for two different lines of C code in two different C files!

However the code in your example is not optimized (unless the structure you named InsertStruct is volatile).

In this case you could compile your code with debugging information switched on and extract the debugging information: From an ELF format file you can use tools like addr2line (freeware from the GNU compiler suite) to check which line of C code corresponds to an instruction at a certain address.

(Note: The addr2line tool is architecture independent as long as both architectures have same width (32-bit), the same endianness and both use the ELF file format; you could use addr2line for ARM to get the information from a TriCore file.)

If you really have to understand a fragment of assembler code I myself typically do the following:

I start a text editor and paste in the assembler code:

movh.a  a15,#@his(InsertStruct)
ld.bu   d15,[a15]@los(InsertStruct)
or      d15,#1
st.b    [a15]@los(InsertStruct),d15
...

Then I replace each instruction by the pseudo-code equivalent:

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

In the next step I try to simplify this code:

a15 =  ((unsigned)&InsertStruct) & 0xFFFF0000;

Then:

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

Then:

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

Then:

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

In the end I try to replace jump instructions:

d15 = 0;
if(d14 == d13) goto L123;
d15 = 1;
L123:

... becomes:

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

... and finally (maybe):

d15 = (d14 != d13);

In the end you have C code in the text editor.

Unfortunately this takes much time - but I don't know any faster method.

Bradleybradly answered 18/12, 2017 at 11:58 Comment(2)
You're somewhat overcomplicating the stuff with a15. You don't really think of those instructions as actually shifting around the address with C operators, do you? I mean, unsigned char *a15 = hi(&InsertStruct) / unsigned d15 = a15[lo(InsertStruct)]. Or just go directly to more reasonable C like d15 = InsertStruct.SomeMember; which is how you'd think of it once you understand how compilers deal with static address constants on machines with fixed-width instructions.Shippy
@ Martin Rosenau Thank you for taking the time to answer. I'm accepting this as the answer, considering you provided two methods I could possibly use to overcome my problem. I doub't it will help much without previous knowledge of assembler, but it's better than nothing. Thanks again.Solubility

© 2022 - 2024 — McMap. All rights reserved.