4

I'm testing several decompilers about struct reconstruction, given the following C example:

struct S {
    int x;
    int y;
    long z;
    long t;
};

int foo(struct S s) {
    return s.x + s.y + s.z + s.t;
}

int main() {
    struct S s;
    s.x = 10; s.y = 15; s.z = 20; s.t = 25;
    return foo(s);
}

compiled without any optimization (even no stripping) using clang as a 64-bit ELF, i.e. the ABI is System V x86-64.

I've supposed that this is a trivial case so the decent decompilers should give correct results, they are not unfortunately.

Following result is given by IDA 7.4.191122:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // r8
  __int64 v4; // r9

  return foo(*(__int64 *)&argc, (__int64)argv, (__int64)envp, 20LL, v3, v4, 0xF0000000ALL, 20, 25);
}

__int64 __fastcall foo(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, int a8, int a9)
{
  return (unsigned int)(a9 + a8 + HIDWORD(a7) + a7);
}

Next, JEB 3.7.0:

unsigned long main() {
  return foo();
}

unsigned long foo() {
  unsigned int v0 = v1 + v2;
  return (unsigned long)(((unsigned int)(((long)v0 + v3 + v4)));
}

and Ghidra 9.1

void main(void)
{
  foo();
  return;
}

ulong foo(void)
{
  int param_7;
  undefined8 param_7_00;
  int iStack000000000000000c;
  long param_8;
  long param_9;

  return (param_7 + iStack000000000000000c) + param_8 + param_9 & 0xffffffff;
}

I cannot say that the results are "good", they are not even correct. Did I miss some configuration for these decompilers?

Edit: Because of request from @Tobias, I've added assembly code for the functions (and changed main into bar):

This is foo:

0x0         55                                   push rbp
0x1         48 89 e5                             mov rbp, rsp
0x4         48 8d 45 10                          lea rax, [rbp+0x10]
0x8         8b 08                                mov ecx, [rax]
0xa         03 48 08                             add ecx, [rax+0x8]
0xd         48 63 d1                             movsxd rdx, ecx
0x10        48 03 50 10                          add rdx, [rax+0x10]
0x14        48 03 50 18                          add rdx, [rax+0x18]
0x18        48 0f be 40 04                       movsx rax, byte ptr [rax+0x4]
0x1d        48 01 c2                             add rdx, rax
0x20        89 d0                                mov eax, edx
0x22        5d                                   pop rbp
0x23        c3                                   ret

and bar:

0x30        55                                   push rbp
0x31        48 89 e5                             mov rbp, rsp
0x34        48 83 ec 40                          sub rsp, 0x40
0x38        c7 45 e0 0a 00 00 00                 mov dword ptr [rbp-0x20], 0xa
0x3f        c7 45 e8 0f 00 00 00                 mov dword ptr [rbp-0x18], 0xf
0x46        48 c7 45 f0 14 00 00 00              mov qword ptr [rbp-0x10], 0x14
0x4e        48 c7 45 f8 19 00 00 00              mov qword ptr [rbp-0x8], 0x19
0x56        c6 45 e4 1e                          mov byte ptr [rbp-0x1c], 0x1e
0x5a        48 8d 45 e0                          lea rax, [rbp-0x20]
0x5e        48 8b 08                             mov rcx, [rax]
0x61        48 89 0c 24                          mov [rsp], rcx
0x65        48 8b 48 08                          mov rcx, [rax+0x8]
0x69        48 89 4c 24 08                       mov [rsp+0x8], rcx
0x6e        48 8b 48 10                          mov rcx, [rax+0x10]
0x72        48 89 4c 24 10                       mov [rsp+0x10], rcx
0x77        48 8b 40 18                          mov rax, [rax+0x18]
0x7b        48 89 44 24 18                       mov [rsp+0x18], rax
0x80        e8 7b ff ff ff                       call foo
0x85        48 83 c4 40                          add rsp, 0x40
0x89        5d                                   pop rbp
0x8a        c3                                   ret
2
  • This question (Are decent decompilers really bad in reconstructing structs, or I missed some config?) is really eliciting opinions, which is discouraged. But seeing the question got an answer (accepted by you) of what seems to be the underlying question, there's no need for me to intervene. Just keep in mind that asking for opinions is usually a bad sign for a question on RE.SE (and other SE sites).
    – 0xC0000022L
    Commented Dec 6, 2019 at 8:32
  • You are right. I've rephrased the question. Sorry. Commented Dec 6, 2019 at 14:26

2 Answers 2

3

The default compilation options do not embed full debugging information, and a small structure passed by value is indistinguishable from a bunch of individual arguments passed in registers (see the ABI spec). You will get a slightly better output if you enable DWARF debug info generation (-gdwarf). At least IDA can make use of DWARF info to import types, apply function arguments and local variable information:

int __cdecl foo(S s)
{
  return LODWORD(s.t) + LODWORD(s.z) + s.y + s.x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
  S v4; // [rsp+0h] [rbp-40h]

  *(_QWORD *)&v4.x = 0xF0000000ALL;
  v4.z = 20LL;
  v4.t = 25LL;
  return foo(v4);
}
2
  • Thank you for DWARF debug info. But I'm not sure about the indistingguishability of struct passing. The System V ABI x86-64 (Section 3.2.3) says that a struct of more than 8 bytes or containing unaligned data will be of MEMORY class, i.e. its fields will be passed/returned via stack. Commented Nov 29, 2019 at 16:09
  • Why did the decompiler recognize that rdi, rsi, ... are not initiialized in main, so they would not be used as parameters for foo? Commented Nov 29, 2019 at 16:18
5

There are several things in your example that makes it hard to decompile.

s is the first, and only, local (so on the stack) variable in main(). main() is troublesome, as it's more or less a vararg-function if you read the C++ standard, and as you can see atleast IDA guesses that you have three arguments on the stack.

You use both int and long in your struct definition, which may or may not create either padding of the stack or masking in the generated code. It can also be one way where you declare it (main) and another way when passing it by value to a (leaf-)function.

And, foo() is a leaf-function, meaning it will have a red-zone on the stack that could possibly be used.

Try putting s on the heap instead, and you will probably see a very different result :)

What does the disassembly look like?

Edit: Oh the disassembly really drives the point home! The point being that LLVM depends on how well-suited the IR is to optimization, as before optimization the code looks like someone who licks rocks built it from lego. And then threw the rock at it :D No wonder it confuses decompilers :) Look at that funny byte-size "bonus parameter" and the "nonsensical" movsx-instructions for example.

Anyways, serious face-time again. The red zone isn't used. The prologue isn't even needed as nothing is stored on the stack, all calculations are done on RCX and RAX. Now that you've gotten rid of any stack-variables in main(), the thing tripping you up is that you're passing a small, stack-allocated, structure by value. What in C looks like passing a single blob as argument is actually treating each field like a separate argument. I am guessing both IDA and Ghidra would be able to make sense of this if it wasn't for the "alignment"(?)-byte thrown in there. Or perhaps not, as the assembly might still look like it's passing four separate arguments on the stack :|

Tl;dr: clang generates really strange code unless optimized. Coupled with passing a stack-allocated struct by value it will confuse the hell out of both decompilers and sleepy reverse engineers such as myself. Take this opportunity to kick the habit of passing structs by-value and learn to love the const-refs ;)

7
  • Thank you, I've added disassembled code. I've changed main to a normal function bar to avoid the troubesomeness of main. Commented Nov 30, 2019 at 10:31
  • I've never thought of red-zone (I didn't know it). Indeed, far seems to use red-zone since there is no stack frame allocation in the function (no substraction from rsp). But I still don't understand why it makes difficult to decompilers: are local variables still based on rbp? Commented Nov 30, 2019 at 10:35
  • Change main to bar does not help, the results are more or less the same. But putting s on heap gives a more clear result, it is expected. Commented Nov 30, 2019 at 10:39
  • @TaThanhDinh As I really don't know if you get notified on updated answers: I updated the answer.
    – Tobias
    Commented Nov 30, 2019 at 12:36
  • @ta-thanh-dinh I just realized I didn't reply to the question about local variables and RBP; No, frame pointers are rarely used in x86-64 code, as both the IA64 and AMD64 ABIs define functions, and their stack layouts, strict enough that you don't really need one in most cases.
    – Tobias
    Commented Dec 2, 2019 at 20:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.