4

In practicing Ghidra on a few standard library functions in some executable, I have generally had trouble getting good decompilation output where SSE registers are used as an optimization. I've tried searching for a variety of keyword combinations both here and on the rest of the internet but could not find anything relevant.

The below examples are from x64 code without debug symbols, so function/struct/variable names are my doing. The type choices did not change/improve the demonstrated issues - but maybe I'm just doing it wrong. I also tried to keep enough context for readability, but I couldn't locate any guidelines this site has in that regard.

  1. When a 32 byte std::string is copied (as part of what looks like move construction) using two 128 bit SSE moves, the entire thing is decomposed into 4 byte chunks:

    My std::string data type definition for completeness (exported):

    struct std_string {
      char * data;
      char * field_1;
      ulonglong size;
      ulonglong capacity;
    };
    

    Disassembly:

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             std_string * __fastcall std_string_operator+(std_string 
             std_string *      RAX:8          <RETURN>
             std_string *      RCX:8          thisOut
             undefined8        RDX:8          thisIn
             char *            R8:8           stringIn                                XREF[1]:     140106dcc(W)  
             longlong          R8:8           size                                    XREF[1]:     140106dcc(W)  
             undefined8        RAX:8          thisIn_                                 XREF[1]:     140106de6(W)  
                             std_string_operator+
       140106dc0 40 53           PUSH       RBX
       140106dc2 48 83 ec 20     SUB        RSP,0x20
       140106dc6 49 8b c0        MOV        RAX,stringIn
       140106dc9 4c 8b ca        MOV        R9,thisIn
       140106dcc 49 c7 c0        MOV        size,-0x1
                 ff ff ff ff
       140106dd3 48 8b d9        MOV        RBX,thisOut
                             LAB_140106dd6                                   XREF[1]:     140106dde(j)  
       140106dd6 49 ff c0        INC        size
       140106dd9 42 80 3c        CMP        byte ptr [RAX + size*0x1],0x0
                 00 00
       140106dde 75 f6           JNZ        LAB_140106dd6
       140106de0 48 8b d0        MOV        thisIn,RAX
       140106de3 49 8b c9        MOV        thisOut,R9
       140106de6 e8 75 fe        CALL       std_string_append                                std_string * std_string_append(s
                 ff ff
       140106deb 33 c9           XOR        thisOut,thisOut
       140106ded 48 89 4b 10     MOV        qword ptr [RBX + 0x10],thisOut
       140106df1 48 89 4b 18     MOV        qword ptr [RBX + 0x18],thisOut
       140106df5 0f 10 00        MOVUPS     XMM0,xmmword ptr [thisIn_]
       140106df8 0f 11 03        MOVUPS     xmmword ptr [RBX],XMM0
       140106dfb 0f 10 48 10     MOVUPS     XMM1,xmmword ptr [thisIn_ + 0x10]
       140106dff 0f 11 4b 10     MOVUPS     xmmword ptr [RBX + 0x10],XMM1
       140106e03 48 89 48 10     MOV        qword ptr [thisIn_ + 0x10],thisOut
       140106e07 48 c7 40        MOV        qword ptr [thisIn_ + 0x18],0xf
                 18 0f 00 
                 00 00
       140106e0f 88 08           MOV        byte ptr [thisIn_],thisOut
       140106e11 48 8b c3        MOV        thisIn_,RBX
       140106e14 48 83 c4 20     ADD        RSP,0x20
       140106e18 5b              POP        RBX
       140106e19 c3              RET
    

    Decompilation:

    std_string * std_string_operator+(std_string *thisOut,std_string *thisIn,char *stringIn)
    {
      undefined4 uVar1;
      undefined4 uVar2;
      undefined4 uVar3;
      std_string *thisIn_;
      longlong size;
    
      size = -1;
      do {
        size = size + 1;
      } while (stringIn[size] != '\0');
      thisIn_ = std_string_append(thisIn,stringIn,size);
      thisOut->size = 0;
      thisOut->capacity = 0;
      uVar1 = *(undefined4 *)((longlong)&thisIn_->data + 4);
      uVar2 = *(undefined4 *)&thisIn_->field_1;
      uVar3 = *(undefined4 *)((longlong)&thisIn_->field_1 + 4);
      *(undefined4 *)&thisOut->data = *(undefined4 *)&thisIn_->data;
      *(undefined4 *)((longlong)&thisOut->data + 4) = uVar1;
      *(undefined4 *)&thisOut->field_1 = uVar2;
      *(undefined4 *)((longlong)&thisOut->field_1 + 4) = uVar3;
      uVar1 = *(undefined4 *)((longlong)&thisIn_->size + 4);
      uVar2 = *(undefined4 *)&thisIn_->capacity;
      uVar3 = *(undefined4 *)((longlong)&thisIn_->capacity + 4);
      *(undefined4 *)&thisOut->size = *(undefined4 *)&thisIn_->size;
      *(undefined4 *)((longlong)&thisOut->size + 4) = uVar1;
      *(undefined4 *)&thisOut->capacity = uVar2;
      *(undefined4 *)((longlong)&thisOut->capacity + 4) = uVar3;
      thisIn_->size = 0;
      thisIn_->capacity = 0xf;
      *(undefined *)&thisIn_->data = 0;
      return thisOut;
    }
    

    I would understand copying the four fields of eight bytes, or (in some form or another) expressing two 128 or one 256 bit copy. I assume the above four byte chunks is how MOVUPS is encoded in Ghidra, but it doesn't seem very helpful to me at all. This kind of copy (via SSE) happens relatively often everywhere, so having 16 lines of noise every time is annoying.

    (Also, don't ask me about the funky function signature, I have no idea what the compiler was on about.)


  1. A PUNPCKLBW used in memset (which here just fills XMM0 by repeating each of the lower 8 bytes, effectively broadcasting the single byte to set to all 16 bytes of XMM0) blows up into dozens of lines of gibberish (which faithfully emulates the effect, I'm sure, but it's not helpful):

    Disassembly:

                         **************************************************************
                         *                          FUNCTION                          *
                         **************************************************************
                         longlong * __fastcall memset(void * location, byte byteT
         longlong *        RAX:8          <RETURN>
         void *            RCX:8          location
         byte              DL:1           byteToSet                               XREF[1]:     1411960a8(W)  
         ulonglong         R8:8           count
         undefined8        R9:8           inputByteRepeated8                      XREF[1]:     1411960a0(W)  
         undefined2        DX:2           inputByteRepeated2                      XREF[1]:     1411960a8(W)  
                         memset                                          XREF[518]:  [...]
     141196090 4c 8b d9        MOV        R11,location
     141196093 0f b6 d2        MOVZX      EDX,DL
     141196096 49 b9 01        MOV        R9,0x101010101010101
               01 01 01 
               01 01 01 01
     1411960a0 4c 0f af ca     IMUL       R9,RDX
     1411960a4 49 83 f8 10     CMP        R8,0x10
     1411960a8 0f 86 f2        JBE        LAB_1411961a0
               00 00 00
     1411960ae 66 49 0f        MOVQ       XMM0,R9
               6e c1
     1411960b3 66 0f 60 c0     PUNPCKLBW  XMM0,XMM0
                          [...]
    

    Decompilation:

    longlong * memset(void *location,byte byteToSet,ulonglong count)
    {
      // [...]
      ushort inputByteRepeated2;
      ulonglong inputByteRepeated8;
      undefined4 uVar5;
      undefined4 uVar7;
      undefined4 uVar8;
      undefined auVar6 [13];
    
      inputByteRepeated8 = (ulonglong)byteToSet * 0x101010101010101;
      inputByteRepeated2 = (ushort)inputByteRepeated8;
      _inputByteRepeated2 = (uint)inputByteRepeated8;
      if (count < 0x11) {
        // [...]
      }
      auVar6[6] = SUB141(ZEXT814(inputByteRepeated8) >> 0x30,0);
      auVar6 = ZEXT813(inputByteRepeated8);
      register0x0000120c =
           SUB164(CONCAT313(SUB163(CONCAT214(SUB162(CONCAT115(SUB161(ZEXT816(inputByteRepeated8) >> 0x38
                                                                     ,0),
                                                              CONCAT114(SUB151(ZEXT815(
                                                      inputByteRepeated8) >> 0x38,0),
                                                      ZEXT814(inputByteRepeated8))) >> 0x70,0),
                                             CONCAT113(auVar6[6],auVar6)) >> 0x68,0),
                            CONCAT112(auVar6[6],ZEXT812(inputByteRepeated8))) >> 0x60,0);
      _auVar6 = CONCAT79(SUB167(CONCAT610(SUB166(CONCAT511(SUB165(CONCAT412(register0x0000120c,
                                                                            CONCAT111(SUB131(auVar6 >> 
                                                      0x28,0),ZEXT811(inputByteRepeated8))) >> 0x58,0),
                                                      CONCAT110(SUB121(ZEXT812(inputByteRepeated8) >>
                                                                       0x28,0),
                                                                (unkuint10)inputByteRepeated8)) >> 0x50,
                                                 0),
                                          CONCAT19(SUB131(auVar6 >> 0x20,0),(unkuint9)inputByteRepeated8
                                                  )) >> 0x48,0),
                         (unkuint9)inputByteRepeated8 & 0xffffffffffffffff | 0 << 0x40);
      register0x00001208 = SUB168(_auVar6 >> 0x40,0);
      _auVar6 = CONCAT115(SUB1611(CONCAT106(SUB1610(CONCAT97(SUB169(CONCAT88(register0x00001208,
                                                                             (inputByteRepeated8 >> 0x18
                                                                             ) << 0x38) >> 0x38,0),
                                                             ((uint7)inputByteRepeated8 >> 0x18) << 0x30
                                                            ) >> 0x30,0),
                                            ((uint6)inputByteRepeated8 >> 0x10) << 0x28) >> 0x28,0),
                          ((uint5)inputByteRepeated8 >> 0x10) << 0x20);
      _auVar6 = CONCAT142(SUB1614(CONCAT133(SUB1613(CONCAT124(SUB1612(_auVar6 >> 0x20,0),
                                                              (_inputByteRepeated2 >> 8) << 0x18) >>
                                                    0x18,0),((uint3)inputByteRepeated8 >> 8) << 0x10) >>
                                  0x10,0),inputByteRepeated2 & 0xff | inputByteRepeated2 << 8);
      uVar7 = SUB164(_auVar6 >> 0x20,0);
      uVar5 = SUB164(_auVar6,0);
      uVar8 = SUB164(_auVar6 >> 0x40,0);
    
      // [...]
    

    The remaining decompilation then also uses these awkward separate 4-byte chunks whenever the code just does a MOVAPS xmmword ptr [location],XMM0 or similar. Took me a while to even recognize this entire function as memcpy as a result!


How can I make the decompiled code more readable in these cases (or avoid the clutter in the first place)?


I'd tag this question with SSE if it already existed as a tag, but I'm reluctant to create it since I don't know this community well enough yet. And of course: Improvement suggestions welcome!

1

One approach to deal with this would be Function Identification feature to automatically detect those functions and just rename them to memcpy accordingly. The issue is that, unlike IDA, Ghidra doesn't ship with a rich library of signatures, though unlike IDA it at least seems feasible to generate your own[0]

You could look into https://blog.threatrack.de/2019/09/20/ghidra-fid-generator and the related github repos with the code that generate the signatures and the already generated signatures and see if those work for your case.

[0] At least I personally never managed to figure out how this works with IDA

| improve this answer | |
  • I appreciate the suggestion for improving my workflow - indeed, I was wondering how many times I'll have to reverse standard library functions in the future. I'm much more curious how to improve the actual decompilation results though. E.g. the string moving noise is really distracting when trying to figure out what some larger function does, is there something to be done about that? – NurTuring May 12 at 19:45
  • Do you mean cases of an explicit memcpy being inlined or C++ initialization shenanigans? For the former there is some feature to declare part of a function as another function that has been inlined (can't quite remember how, might be worth a separate question). For the latter you might need separate scripts or plugins. In that case, could you edit your question with an example of the decompiled code vs the code you would imagine as an ideal result (optionally vs the actual source) ? – Florian Magin May 13 at 9:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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