0

I'm looking through a block of disassembled C++ which works with several std::string instances. I had been confused by several calls to various versions of std::operator+, but this call seems completely wrong (by my understanding anyway):

mov        rax, qword [rbp-0xb8]
lea        rbx, qword [rax+0xa0]
lea        rax, qword [rbp-0x60]
mov        edx, 0x880d32  ; "/store/"
mov        rsi, rax
mov        rdi, rbx
           ; std::string std::operator+(std::string &&, char const *),
call       _ZStplIcSt11char_traitsIcESaIcEENSt7__cxx1112basic_stringIT_T0_T1_EEOS8_PKS5_
lea        rax, qword [rbp-0x60]

From context, I have determined that the stack values referenced are:

  • rbp-0x60: An std::string on the stack, constructed with std::string(char const *, std::allocator<char> &).
  • rbp-0xb8: A pointer to this.

From the .comment section, I can see the compiler used was GCC 5.4.0, from which I retrieved this implementation of the operator+ call above (in namespace std { ... }):

template<typename _CharT, typename _Traits, typename _Alloc>
inline
basic_string<_CharT, _Traits, _Alloc>
operator+(
    basic_string<_CharT, _Traits, _Alloc> &&__lhs,
    const _CharT *__rhs)
{
    return std::move(__lhs.append(__rhs));
}

I can understand the return value being optimized away since __lhs is modified by operator+, but the parameters don't seem to match. edx referring to the only char * suggests an additional first parameter before those declared in the source. If this was a member function, I would expect that (rdi being this), but operator+ is implemented as a non-member.

Am I missing something from the calling convention here?

1

std::operator+ takes two arguments
both pointers to std::string
like std::operator+(_lhs , _rhs ); and returns back _Ans a pointer to concatenated result of_lhs & _rhs
since you are using gcc the first three register that apply are rdi,rsi and rdx

rdi is usually _Ans the resultant std::string address rsi is usually _lhs rdx is usually _rhs

in your disassembly
rdi = rbx--> [rax+0xa0]
rsi = rax--> [rbp-0x60]
rdx = edx-> 0x880d32 moving a const to edx zeroes the upper part of rdx

see result of using unicorn for emulating mov rdx,0xffffffffffffffff,mov edx,1

from __future__ import print_function
from unicorn import *
from unicorn.x86_const import *
# code to be emulated mov rdx,0xffffffffffffffff; mov edx,1
X86_CODE64 = b"\x48\xc7\xc2\xff\xff\xff\xff\xba\x01\x00\x00\x00" 
ADDRESS = 0x1000000000
try:
    mu = Uc(UC_ARCH_X86, UC_MODE_64)
    mu.mem_map(ADDRESS, 2 * 1024 * 1024)
    mu.mem_write(ADDRESS, X86_CODE64)
    mu.emu_start(ADDRESS, ADDRESS + 7)    
    print(">>> RDX = 0x%x" %mu.reg_read(UC_X86_REG_RDX))
    mu.emu_start(ADDRESS+7, ADDRESS + 12)    
    print(">>> RDX = 0x%x" %mu.reg_read(UC_X86_REG_RDX))
    
except UcError as e:
    print("ERROR: %s" % e)

result of register rdx after each step

:\>python test1.py
>>> RDX = 0xffffffffffffffff
>>> RDX = 0x1

for better understanding you can try the code below with compiler of your choice at compiler explorer

#include <iostream>
#include <string>
std::string conc(std::string first,std::string second)
{
    return std::operator+(first,second);
}
int main()
{  
    std::string result =  conc("hello ","world\n"); 
    std::cout << result;
}

compiling for gcc 5.4 disassembly of conc() is as follows

conc(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
  push rbp
  mov rbp, rsp
  sub rsp, 32
  mov QWORD PTR [rbp-8], rdi
  mov QWORD PTR [rbp-16], rsi
  mov QWORD PTR [rbp-24], rdx
  mov rax, QWORD PTR [rbp-8]
  mov rdx, QWORD PTR [rbp-24]
  mov rcx, QWORD PTR [rbp-16]
  mov rsi, rcx
  mov rdi, rax
  call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char, std::char_traits<char>, std::allocator<char> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
  mov rax, QWORD PTR [rbp-8]
  leave
  ret
4
  • rdi is usually _Ans the resultant std::string address rsi is usually _lhs rdx is usually _rhs – My understanding was that in the SystemV calling convention, rax is used for return values, and rdi is the first parameter. Why is that different here?
    – MTCoster
    Mar 9 at 22:47
  • Yes rax is used for return but this is not return. It is an auto argument sent by caller like foo(auto,def,def) look at main and at the prologue upon entry where rdi is saved retrieved manipulated and returned back
    – blabb
    Mar 10 at 4:26
  • in laymans terms maybe you need to ask yourself if i am sending in two predefined,preallocated,presized,unchangeable buffer pointers how a new object that is a size of sum of input buffers at-least will be returned back? where does the memory to hold the resultant come from it either must be dynamically allocated and returned back for the caller to free() after use or must be destructible and preallocated by the caller and passed on
    – blabb
    Mar 10 at 5:00
  • Thank you for this explanation! I found the reference I was looking for in the SystemV ABI document under 3.2.3§Return Values
    – MTCoster
    Mar 10 at 9:46
2

since this is tangentially related to query
I am adding this as another answer instead of editing the first answer
it appears the code in question possibly ignores compiler warnings

<source>: In function 'int main()':
<source>:11:40: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
   11 |     std::cout << foo(std::string("H"), "ello World!\n");
      |                                        ^~~~~~~~~~~~~~~
Compiler returned: 0

so a 32 bit address like 0x880d32 is passed as an argument to a 64 bit program

i was wondering under what circumstances edx would be passed instead of rdx
so i demangled the

 _ZStplIcSt11char_traitsIcESaIcEENSt7__cxx1112basic_stringIT_T0_T1_EEOS8_PKS5_  

which resulted in

std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char, std::char_traits<char>, std::allocator<char> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&, char const*) 

so the code in question actually uses an Rvalue Reference Declaration

c++11 feature gccrvalueref , msvcrvalueref
and instead of passing a reference uses an explicit char*
the construction can be ascertained by compiling the code below and looking at disassembly.

code for test

#include <iostream>
#include <string>
std::string foo(std::string _lhs,char *_rhs)
{
    return std::operator+(_lhs , _rhs);
}
int main()
{
    char rval[] = {"ello World!\n"};
    std::cout << foo(std::string("H"), rval);
    std::cout << foo(std::string("H"), "ello World!\n");
    
}

disassembly of first call to foo() uses proper 64 bit rdx

  lea rax, [rbp-176]
  lea rdx, [rbp-189]
  lea rcx, [rbp-144]
  mov rsi, rcx
  mov rdi, rax
  call foo(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, char*)

disassembly of second call to foo() uses a 32 bit offset edx

  lea rax, [rbp-96]
  lea rcx, [rbp-64]
  mov edx, OFFSET FLAT:.LC1 and if linked mov edx,0x402007 a 32 bit address
  mov rsi, rcx
  mov rdi, rax
  call foo(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, char*)
.LC1:
  .string "ello World!\n"

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.