Fixing cross platform mac to linux remote expression evaluation

Hi,

I’ve been looking into why expression evaluation is failing when targeting a remote Linux machine from Mac lldb host and it seems there are only 2 distinct problems remaining - both around memory allocation:

  1. Can’t find symbol for mmap.
  2. Once found, lldb is calling mmap with incorrect constant values for MAP_ANON.

For problem 1, the library being linked against (e.g. /lib/x86_64-linux-gnu/libc-2.19.so) is copied into a local module cache, but we don’t copy the unstripped library in /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.19.so (I’m assuming we can’t call mmap from the symtab file given SymbolFileSymtab::FindFunctions returns 0). To avoid having to duplicate the symbol discovery (in Symbols::LocateExecutableSymbolFile) we should probably ask lldb-platform on the target to find the symbol files for the current target (I’m thinking Platform::ResolveSymbolFile looks like the right place).

For problem 2, we’re building the argument list to mmap and the constant for MAP_ANON on macosx is 0x1000 whereas for linux it’s 0x20. I’m not sure what the right way to fix this is, I could imagine asking Platform to allocate memory, but this would likely be an involved change, or perhaps being able to ask platform for various OS specific const values which would be hard-coded into it when built for the target.

Anyways, I wanted to send this out to see if anyone had any thoughts on either of these issues or was already working on them. I have verified (by hacking in the correct const values for linux and placing debug libs in a path where they will be found) that this fixes expression evaluation (and 14 tests start passing) for mac->linux debugging.

Thanks in advance for any suggestions,
Rob

P.S. the 14 tests passing mac->linux by fixing this (for other people looking at cross platform tests):

Test11588.py
TestAnonymous.py
TestBreakpointConditions.py
TestCPPStaticMethods.py
TestCStrings.py
TestCallStdStringFunction.py
TestDataFormatterCpp.py
TestDataFormatterStdList.py
TestExprDoesntBlock.py
TestExprHelpExamples.py
TestFunctionTypes.py
TestPrintfAfterUp.py
TestSBValuePersist.py
TestSetValues.py

Hi,

I've been looking into why expression evaluation is failing when targeting a remote Linux machine from Mac lldb host and it seems there are only 2 distinct problems remaining - both around memory allocation:

1. Can't find symbol for mmap.
2. Once found, lldb is calling mmap with incorrect constant values for MAP_ANON.

For problem 1, the library being linked against (e.g. /lib/x86_64-linux-gnu/libc-2.19.so) is copied into a local module cache, but we don't copy the unstripped library in /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.19.so (I'm assuming we can't call mmap from the symtab file given SymbolFileSymtab::FindFunctions returns 0). To avoid having to duplicate the symbol discovery (in Symbols::LocateExecutableSymbolFile) we should probably ask lldb-platform on the target to find the symbol files for the current target (I'm thinking Platform::ResolveSymbolFile looks like the right place).

For problem 2, we're building the argument list to mmap and the constant for MAP_ANON on macosx is 0x1000 whereas for linux it's 0x20. I'm not sure what the right way to fix this is, I could imagine asking Platform to allocate memory, but this would likely be an involved change, or perhaps being able to ask platform for various OS specific const values which would be hard-coded into it when built for the target.

So we need to implement the allocate and deallocate memory packets in lldb-server. It seems we have it implemented in the client, but not in the server:

addr_t
GDBRemoteCommunicationClient::AllocateMemory (size_t size, uint32_t permissions)
{
    if (m_supports_alloc_dealloc_memory != eLazyBoolNo)
    {
        m_supports_alloc_dealloc_memory = eLazyBoolYes;
        char packet[64];
        const int packet_len = ::snprintf (packet, sizeof(packet), "_M%" PRIx64 ",%s%s%s",
                                           (uint64_t)size,
                                           permissions & lldb::ePermissionsReadable ? "r" : "",
                                           permissions & lldb::ePermissionsWritable ? "w" : "",
                                           permissions & lldb::ePermissionsExecutable ? "x" : "");
        assert (packet_len < (int)sizeof(packet));
        StringExtractorGDBRemote response;
        if (SendPacketAndWaitForResponse (packet, packet_len, response, false) == PacketResult::Success)
        {
            if (response.IsUnsupportedResponse())
                m_supports_alloc_dealloc_memory = eLazyBoolNo;
            else if (!response.IsErrorResponse())
                return response.GetHexMaxU64(false, LLDB_INVALID_ADDRESS);
        }
        else
        {
            m_supports_alloc_dealloc_memory = eLazyBoolNo;
        }
    }
    return LLDB_INVALID_ADDRESS;
}

bool
GDBRemoteCommunicationClient::DeallocateMemory (addr_t addr)
{
    if (m_supports_alloc_dealloc_memory != eLazyBoolNo)
    {
        m_supports_alloc_dealloc_memory = eLazyBoolYes;
        char packet[64];
        const int packet_len = ::snprintf(packet, sizeof(packet), "_m%" PRIx64, (uint64_t)addr);
        assert (packet_len < (int)sizeof(packet));
        StringExtractorGDBRemote response;
        if (SendPacketAndWaitForResponse (packet, packet_len, response, false) == PacketResult::Success)
        {
            if (response.IsUnsupportedResponse())
                m_supports_alloc_dealloc_memory = eLazyBoolNo;
            else if (response.IsOKResponse())
                return true;
        }
        else
        {
            m_supports_alloc_dealloc_memory = eLazyBoolNo;
        }
    }
    return false;
}

Then you call mmap yourself on the native machine in lldb-server instead of trying to know what enums will work.

We actually need to ask the PlatformLinux to run an allocate/deallocate memory and hand it a process. So we can add the following to lldb_private::Platform:

    virtual bool
    SupportsMemoryAllocation();

    virtual lldb::addr_t
    AllocateMemory (lldb_private::Process *process, size_t size, uint32_t permissions, Error &error);

    virtual Error
    DeallocateMemory (lldb_private::Process *process, lldb::addr_t ptr);

Then the lldb_private::Process can get the current platform and ask it if it supports allocating memory, and if so call the Platform::AllocateMemory()/Platform:: DeallocateMemory().

Then the PlatformLinux can "do the right thing" and use the right defines.

Anyways, I wanted to send this out to see if anyone had any thoughts on either of these issues or was already working on them. I have verified (by hacking in the correct const values for linux and placing debug libs in a path where they will be found) that this fixes expression evaluation (and 14 tests start passing) for mac->linux debugging.

Thanks in advance for any suggestions,
Rob

So my suggestion is to implement the memory allocation/deallocation in lldb-server since it runs natively and will avoid the problems we run into by trying to evaluate functions by calling them remotely using #define values from the current system...

Hi,

this may not be so important if we choose the approach Greg suggests
(which sounds like a good idea), but in any case, I wanted to say that
I don't think that we should be depending on the debug symbols in libc
for a basic functionality like this. I think it's not safe to assume
that debug symbols will be available on every machine. And in any
case, mmap is a public symbol in libc, so it should be possible to
find it without debug symbols:
$ objdump -T /lib/libc-2.20.so | grep mmap
00000000000e3670 w DF .text 0000000000000024 GLIBC_2.2.5 mmap64
00000000000e3670 w DF .text 0000000000000024 GLIBC_2.2.5 mmap

What does objdump produce on your machine? If mmap is there, and lldb
is not finding it then I think we should find out why...

cheers,
pl

Thanks Greg for the suggestions, I’ll start working on that and put up a patch when I have something working.

Even doing it this way though, I imagine we’ll still want to be able to load debug info for standard libraries if we can find it. I’m guessing we can only call functions from the symbol table if we have the function spec provided elsewhere (i.e. from a header file) or we know exactly what the function arguments are (as was the case in lldb_private::InferiorCallMmap).

Also sending to the list....

Thanks Greg for the suggestions, I'll start working on that and put up a patch when I have something working.

Even doing it this way though, I imagine we'll still want to be able to load debug info for standard libraries if we can find it. I'm guessing we can only call functions from the symbol table if we have the function spec provided elsewhere (i.e. from a header file) or we know exactly what the function arguments are (as was the case in lldb_private::InferiorCallMmap).

mmap is a symbol every app needs to be able to link to. We don't need debug info to call it. We just need to be able to find the symbol for it so we know where the code for mmap is.

Hi,

this may not be so important if we choose the approach Greg suggests
(which sounds like a good idea), but in any case, I wanted to say that
I don't think that we should be depending on the debug symbols in libc
for a basic functionality like this. I think it's not safe to assume
that debug symbols will be available on every machine. And in any
case, mmap is a public symbol in libc, so it should be possible to
find it without debug symbols:
$ objdump -T /lib/libc-2.20.so | grep mmap
00000000000e3670 w DF .text 0000000000000024 GLIBC_2.2.5 mmap64
00000000000e3670 w DF .text 0000000000000024 GLIBC_2.2.5 mmap

What does objdump produce on your machine? If mmap is there, and lldb
is not finding it then I think we should find out why...

I'd like to not depend on the debug symbols, but we should definitely load them if we can find them (as we do when running locally).

Agreed. We don't require them, but it will be nice to have them.

The symbol table is in the stripped .so (readelf -s or objdump -T show the symbol entries for mmap), but we're calling SymbolFileSymtab::FindFunctions which returns 0 because we don't have the full method info. When called from lldb_private::InferiorCallMmap, it seems like all we need is the base address which we could get that from the symbol table. To support general function calls into those functions though, as far as I can tell we can't tell from the symbol table what arguments are required, only the base address. I haven't tested this yet but I'm hoping that if the system header file for that function is included we'll find the function spec in the target's debug info.

Again, if "mmap" can be looked up via:

void *mmap_address = dlsym(..., "mmap");

Then there must be a public symbol available in the libc.so. If LLDB doesn't see this, we need to fix that, even without debug symbols.

Please let me know if I’m missing something, as far as I can see lldb-server does not have a Process object, only a NativeProcessLinux (which is a NativeProcessProtocol). This doesn’t seem to have the context to run the thread plan to call malloc and allocate memory from lldb-server - see the comment in NativeProcessLinux::AllocateMemory and lldb_private::InferiorCallMmap / Process::RunThreadPlan for the full details of what it needs to do. I’m not completely familiar with how the interaction between lldb and lldb-server works, and I can try to implement a function call with a NativeProcessLinux (preserving the previous state) but it seems like this will be reimplementing a lot of functionality to do so naively. Any thoughts? Is there a simple way to call malloc in the context of the NativeProcessLinux and get the return that I’m missing?

You need to use mmap() because that is the only way to change the type of the memory to be executable. If you use malloc, you get read+write memory and can't change it to restrict the type. Allocate memory can request read, write and execute (any combo of the three).

Sorry I keep mistakenly writing malloc instead of mmap. The problem is still that the lldb-server only instantiates a NativeProcessLinux, which doesn’t extend Process - i.e. it doesn’t have an ABI object or Target object, so we can’t use it to create and execute a thread plan to run the call to mmap and get the return value. In fact, we don’t even initialize any of the ABI plugins on lldb-server. We do have to run mmap from the inferior process right?

Assuming we must generate the instructions from the host, how about adding a constants map to the qHostInfo response? Alternately, how about just hand-copying the few constants we need to some function in InferiorCallPOSIX and switching based on the Target triple?

Sorry I keep mistakenly writing malloc instead of mmap. The problem is still that the lldb-server only instantiates a NativeProcessLinux, which doesn't extend Process - i.e. it doesn't have an ABI object or Target object, so we can't use it to create and execute a thread plan to run the call to mmap and get the return value. In fact, we don't even initialize any of the ABI plugins on lldb-server. We do have to run mmap from the inferior process right?

Sorry, I forgot that we have the ability on MacOSX to allocate memory with different permissions in another process. So we can call functions in debugserver that can allocate memory in the child process. Not sure if this is possibly on Linux. It would be worth looking into.

If we need to run an expression, this would need to be done from LLDB, not down in lldb-server and certainly not in NativeProcessLinux as you were figuring out.

Assuming we must generate the instructions from the host, how about adding a constants map to the qHostInfo response? Alternately, how about just hand-copying the few constants we need to some function in InferiorCallPOSIX and switching based on the Target triple?

The constants for mmap won't change, so I would just actually code this up in the PlatformLinux and have the platform allocate memory by calling mmap with the correct option values. We might be able to code this up in PlatformPOSIX make an expression using the right values based on the triple, or we can code different versions up in each different Platform.

Sorry for the confusion, I was thinking we would just need to call mmap() from within lldb-server and that isn't the case unless there is some other trick to allocate memory in another process in Linux...

Not sure if this would work:

http://man7.org/linux/man-pages/man2/process_vm_readv.2.html

You might be able to allocate memory using mmap in the lldb-server process and then share the memory pages with the child process somehow.

Greg

Hi Rob,

I investigated this issue back when we first setup the expression evaluation on Linux and have a few observations. There is a special packet in the lldb protocol (not sure if it is in the gdb protocol also) for allocateing memory what is first tried when we need memory in the inferior’s address sapce (if it failes we fall back to InferiorCallMmap). It is implemented with a special system call (I guess) on OSX but it isn’t available on Linux so we have to implement it with calling mmap in the inferior. Using the thread plans in lldb-server isn’t a possible option due to the fact that we don’t have a Process instance in lldb-server and if we can get around that issue then using it would increase the size of lldb-server by a lot (it will depend on a lot of code from clang and/or llvm). ds2 (debugserver implementation for Android from Facebook, available on github) inject some hand written byte code for calling mmap into the inferior with ptrace calls and execute them the same way as the thread plan would do on the target. It is a bit messy solution as we have to write byte code (assembly) for each supported architecture but don’t have significant size penalty and decrease the number of round trips between lldb and lldb-server for memory allocations. The third option is the one you suggested with adding some logic to the Target or to the Platform to query the value of different (pre decided) flags based on the target triple or with an lldb packet. Personally I would suggest to change lldb to use the right constant values in InferiorCallMmap based on the target triple as writing byte code and then injecting it into the inferior’s memory in lldb-server is a bit complicated, very hard to debug if something went wrong and have only minor benefits (number of round trips are much higher in other part of the code).

Tamas

I agree with this. We used to use the InferiorCallMMap on OS X, and that method works fine. Actually it took a bit of effort to get it working since you have to stop to call a function while in the middle of setting up the Thread Plan that is running the expression you really want to call, which took a bit of fancy-footing. But that should be pretty solid now.

Getting the debugserver to do it locally is higher performance, but probably not enough to make much difference, certainly not enough to warrant all the effort of hand-coding it in lldb-server.

Jim