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.
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.)
A
PUNPCKLBW
used in memset (which here just fillsXMM0
by repeating each of the lower 8 bytes, effectively broadcasting the single byte to set to all 16 bytes ofXMM0
) 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 asmemcpy
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!