This is an archive of the discontinued LLVM Phabricator instance.

Have MemoryCache cache addresses that were unreadable, so duplicate read attempts can be suppressed
Needs ReviewPublic

Authored by jasonmolenda on Sep 29 2022, 1:57 PM.

Details

Summary

This patch is to address an issue hit by one of our heavy SB API scripting developers; they have some code iterating over a large number of objects in SBValue's and one of the elements of the object has a type of void*; the Itanium ABI is trying to sniff the memory there to see if it might have a dynamic type. In this case, the elements often have the value 0, so lldb is trying to read memory at address 0, which fails, and the number of reads is quite large. Each failing memory read takes long enough in a JTAG like environment that this is a big perf issue.

We have a MemoryCache subsystem that saves blocks of inferior memory when we do a read, so repeated reads of the same address, or reads near the address that were cached, are saved at a single point in time. These memory cache buffers are flushed when execution is resumed.

This patch adds a new list of addresses we've tried to read from at this execution stop, which returned an error, and will not try to read from those addresses again. If lldb allocates memory in the inferior, or if we resume execution, this list of addresses is flushed.

I settled on using an llvm::SmallSet container for these addr_t's, but I'd appreciate if people have a different suggestion for the most appropriate container. I expect this list to be relatively small -- it is not common that lldb tries to read from addresses that are unreadable -- and my primary optimization concern is quick lookup because I will consult this list before I read from any address in memory.

When I was outlining my idea for this, Jim pointed out that MemoryCache has a InvalidRanges m_invalid_ranges ivar already, and could I reuse this. This is called by the DynamicLoader to mark a region of memory as never accessible (e.g. the lower 4GB on a 64-bit Darwin process), and is not flushed on execution resume / memory allocation. It is expressed in terms of memory ranges, when I don't know the range of memory that is inaccessible beyond an address. I could assume the range extends to the end of a page boundary, if I knew the page boundary size, but that still doesn't address the fact that m_invalid_ranges is intended to track inaccessible memory that is invariant during the process lifetime.

Diff Detail

Event Timeline

jasonmolenda created this revision.Sep 29 2022, 1:57 PM
Herald added a project: Restricted Project. · View Herald TranscriptSep 29 2022, 1:57 PM
jasonmolenda requested review of this revision.Sep 29 2022, 1:57 PM
DavidSpickett added a subscriber: DavidSpickett.

Seems like a valid thing to do.

Is it a problem that maybe you read from address N to N+100 and the problem address is actually at N+50, not N? I think you might be handling that already but I didn't read all the logic just your additions. Maybe not an issue because a lot of this is done in pages of memory, unlikely to be a <10 byte gap where something fails (and you can manually flush the cache if that somehow happens).

In this case, the elements often have the value 0, so lldb is trying to read memory at address 0, which fails, and the number of reads is quite large.

In this environment I guess they don't have a dynamic loader to have set the ranges? So in Mac OS userspace, this wouldn't be a problem because the zero page would be marked as always invalid, but they don't have that luxury.

And there's nothing stopping a bare metal program from mapping memory at 0, so one can't just say 0 means invalid 100% of the time.

doesn't address the fact that m_invalid_ranges is intended to track inaccessible memory that is invariant during the process lifetime.

Please add this as a comment, will be good to know if/when there are two lists involved.

lldb/source/Target/Memory.cpp
157

Should this be more specific like "failed because we know this is unreadable" or "has previously failed to read"?

LGTM

I didn't see a more appropriate datatype than SmallSet in the llvm collection.

I wondered about the same thing Dave asked - should the errors mention that this failed because we checked a negative cache - but I think that would be more confusing than helpful to lldb users. If we wanted to track these decisions it would be more appropriate to log them, but I'm not sure even that is necessary.

If we wanted to track these decisions it would be more appropriate to log them, but I'm not sure even that is necessary.

Yeah this is a better idea, if we do it at all.

Let me rephrase the question. If I had a memory read failing and I suspected that the cache was marking it as unreadable how would I confirm that? If there's a way to do that when it's really needed, then we're good.

Perhaps log only when a new address is added to the unreadable list? Then with the memory log plus the packet log you could figure out the issue, even if you didn't know that the cache had this unreadable address feature before you started investigating.

Jim & David, thanks so much for the comments. The most important question of correctness that David asks is whether we might try to read 32k from an address and mark that address as inaccessible because a page 24k into the memory read failed (for instance). I tried to be conservative and only mark an address as inaccessible when we were able to read 0 bytes (ReadMemory returns partial reads).

I think adding a little hint in the error message, "(cached)" would make it clearer to an lldb maintainer what happened, and not distract the user with too much.

If we wanted to track these decisions it would be more appropriate to log them, but I'm not sure even that is necessary.

Yeah this is a better idea, if we do it at all.

Let me rephrase the question. If I had a memory read failing and I suspected that the cache was marking it as unreadable how would I confirm that? If there's a way to do that when it's really needed, then we're good.

When I was testing/debugging this, I did it by turning the packet log on :)

Perhaps log only when a new address is added to the unreadable list? Then with the memory log plus the packet log you could figure out the issue, even if you didn't know that the cache had this unreadable address feature before you started investigating.

Yes, I can add a log message to Process when I add an address, good idea.

Let me go through the patch again carefully to make sure I'm correctly handling partially-successful reads (not putting anything in the inaccessible memory cache) and tweak that error message a tad & add a bit of logging.

labath added a subscriber: labath.Oct 4 2022, 12:02 AM

I don't know about debugserver, but both lldb-server and gdbserver currently return an error when the memory is partially accessible, even though the protocol explicitly allows the possibility of truncated reads. It is somewhat hard to reproduce, because the caching mechanism in lldb aligns memory reads, and the cache "line" size is usually smaller than the page size -- which is probably why this behavior hasn't bothered anyone so far. Nonetheless, I would say that this behavior (not returning partial contents) is a (QoI) bug, but the fact that two stubs have it makes me wonder how many other stubs do the same as well..

jasonmolenda added a comment.EditedOct 4 2022, 3:17 PM

I don't know about debugserver, but both lldb-server and gdbserver currently return an error when the memory is partially accessible, even though the protocol explicitly allows the possibility of truncated reads. It is somewhat hard to reproduce, because the caching mechanism in lldb aligns memory reads, and the cache "line" size is usually smaller than the page size -- which is probably why this behavior hasn't bothered anyone so far. Nonetheless, I would say that this behavior (not returning partial contents) is a (QoI) bug, but the fact that two stubs have it makes me wonder how many other stubs do the same as well..

Hi Pavel, thanks for pointing this out. I did a quick check with debugserver on darwin, using memory region to find the end of an accessible region in memory, and starting a read request a little earlier, in readable memory:

(lldb) sett set target.process.disable-memory-cache true

(lldb) mem region 0x0000000101800000
 <  31> send packet: $qMemoryRegionInfo:101800000#ce
 <  34> read packet: $start:101800000;size:6a600000;#00
[0x0000000101800000-0x000000016be00000) ---

(lldb) mem region 0x0000000101800000-4
 <  31> send packet: $qMemoryRegionInfo:1017ffffc#d8
 < 122> read packet: $start:101000000;size:800000;permissions:rw;dirty-pages:101000000,101008000,10100c000,1017fc000;type:heap,malloc-small;#00
[0x0000000101000000-0x0000000101800000) rw-

(lldb) x/8wx 0x0000000101800000-4

 <  17> send packet: $x1017ffffc,20#ca
 <   8> read packet: $00000000#00

 <  17> send packet: $x101800000,1c#f2
 <   7> read packet: $E80#00

0x1017ffffc: 0x00000000 0x00000000 0x00000000 0x00000000
0x10180000c: 0x00000000 0x00000000 0x00000000 0x00000000
warning: Not all bytes (4/32) were able to be read from 0x1017ffffc.
(lldb)

We ask for 32 bytes starting at 0x1017ffffc, get back 4 bytes. Then we try to read the remaining 28 bytes starting at 0x101800000, and get an error. So this is different behavior from other stubs where you might simply get an error for the request to read more bytes than are readable. This does complicate the approach I'm doing -- because I'll never know which address was inaccessible beyond "something within this address range". I don't think I can do anything here if that's the case.

We ask for 32 bytes starting at 0x1017ffffc, get back 4 bytes. Then we try to read the remaining 28 bytes starting at 0x101800000, and get an error. So this is different behavior from other stubs where you might simply get an error for the request to read more bytes than are readable. This does complicate the approach I'm doing -- because I'll never know which address was inaccessible beyond "something within this address range". I don't think I can do anything here if that's the case.

to be clear, I think I'll need to abandon this.

I don't know about debugserver, but both lldb-server and gdbserver currently return an error when the memory is partially accessible, even though the protocol explicitly allows the possibility of truncated reads. It is somewhat hard to reproduce, because the caching mechanism in lldb aligns memory reads, and the cache "line" size is usually smaller than the page size -- which is probably why this behavior hasn't bothered anyone so far. Nonetheless, I would say that this behavior (not returning partial contents) is a (QoI) bug, but the fact that two stubs have it makes me wonder how many other stubs do the same as well..

Hi Pavel, thanks for pointing this out. I did a quick check with debugserver on darwin, using memory region to find the end of an accessible region in memory, and starting a read request a little earlier, in readable memory:

(lldb) sett set target.process.disable-memory-cache true

(lldb) mem region 0x0000000101800000
 <  31> send packet: $qMemoryRegionInfo:101800000#ce
 <  34> read packet: $start:101800000;size:6a600000;#00
[0x0000000101800000-0x000000016be00000) ---

(lldb) mem region 0x0000000101800000-4
 <  31> send packet: $qMemoryRegionInfo:1017ffffc#d8
 < 122> read packet: $start:101000000;size:800000;permissions:rw;dirty-pages:101000000,101008000,10100c000,1017fc000;type:heap,malloc-small;#00
[0x0000000101000000-0x0000000101800000) rw-

(lldb) x/8wx 0x0000000101800000-4

 <  17> send packet: $x1017ffffc,20#ca
 <   8> read packet: $00000000#00

 <  17> send packet: $x101800000,1c#f2
 <   7> read packet: $E80#00

0x1017ffffc: 0x00000000 0x00000000 0x00000000 0x00000000
0x10180000c: 0x00000000 0x00000000 0x00000000 0x00000000
warning: Not all bytes (4/32) were able to be read from 0x1017ffffc.
(lldb)

We ask for 32 bytes starting at 0x1017ffffc, get back 4 bytes. Then we try to read the remaining 28 bytes starting at 0x101800000, and get an error. So this is different behavior from other stubs where you might simply get an error for the request to read more bytes than are readable.

Yes, that's pretty much what I did, except that I was not able to read any data with the caching turned off.

to be clear, I think I'll need to abandon this.

I don't think this is necessarily a lost cause. I mean, the debugserver behavior (truncated reads) is definitely better here, and the caching of negative acesses makes sense. And, as the example above shows, the current behavior with the non-truncating stubs is already kind of broken, because you cannot read the areas near the end of mapped space without doing some kind of a bisection on the size of the read (we could implement the bisection in lldb, but... ewww).

The safest way to pursue this would be to have the stub indicate (maybe via qSupported) its intention to return truncated reads, and then key the behavior off of that. However, if that's not possible (it seems you have some hardware stub here), I could imagine just enabling this behavior by default. We can definitely change the lldb-server behavior, and for the rest, we can tell them to fix their stubs.

That is, if they even notice this. The memory read alignment hides this problem fairly well. To demonstrate this, we've had to turn the caching off -- but that would also turn off the negative cache, and avoid this problem. So, if someone can't fix their stub, we can always tell them to turn the cache off as a workaround.