Code coverage extension doesn't work with blob loader (baremetal binary emulation)
Describe the bug Code coverage extension particularly drcov (and I'm quite sure ezcov as well) doesn't work with binary emulations that use the blob loader ie baremetal binary emulation.
Sample Code and context
I am working on the dev branch.
My setup is the following one :
I emulate a bare metal binary running on ARM cortex A7 processor. For the global emulation setup I followed this qiling/examples/hello_arm_uboot.py and defined a cortex_a.ql file to setup the memory as required by the blob loader, all this works perfectly.
with open("./baremetal_binary", "rb") as f:
binary = f.read()
ql = Qiling(code=binary[0x00000000:],archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB , verbose=QL_VERBOSE.DISABLED, cputype=ARM_CPU_MODEL.ARM_CORTEX_A7, profile="cortex_a.ql")
[...]
with cov_utils.collect_coverage(ql, 'drcov_exact', 'output.cov'):
ql.run(count=150000)
The problem happens when I try to collect coverage using drcov and drcov_exact : the generated coverage file is empty. No error happens during the execution but still the coverage file is empty.
Expected behavior A non-empty coverage file.
Screenshots Here is the generated coverage file :
DRCOV VERSION: 2
DRCOV FLAVOR: drcov
Module Table: version 2, count 0
Columns: id, base, end, entry, checksum, timestamp, path
BB Table: 0 bbs
Problem Identification and fix suggestions
I worked on the problem and figure out how to solve it but this require different modifications. As this is a particular qiling use case I don't know what you think about the modifications. These are just suggestions to help your work :)
- Problem 1 : in the drcov.py file :
@staticmethod
def block_callback(ql, address, size, self):
for mod_id, mod in enumerate(ql.loader.images):
if mod.base <= address <= mod.end:
ent = bb_entry(address - mod.base, size, mod_id)
self.basic_blocks.append(ent)
break
The for loop is never entered when working with the blob loader because the QlLoaderBLOB has an empty images attribute. Thus, no bb_entry will be appended to the list via the callback and this produce the empty coverage file.
- Problem 2 : another problem has happened after the fix and is not due my fix. I work with a baremetal binary and no OS thus, addresses in the binary are physical addresses. The following necessary translation in other cases is a problem in mine : in the drcov.py file :
ent = bb_entry(address - mod.base, size, mod_id)
The "address - mod.base" substraction required in a classical binary creates a problem because the drcov field that normally looks like (address, size, mod_id) has an address that doesn't match the baremetal memory mapping with physical addresses. Thus, when loading the coverage file in IDA for example, errors happen because the drcov parser doesn't recognise the stored address.
- Fix suggestions : my personnal fix is a working but ugly althought I can still share my reflexions about this. Either you want to let the drcov class most unchanged as possible and this leads to modify the QlLoaderBLOB to initialize images attribute but you will still have to modify the drcov to avoid the address translations based on I don't know which condition. Or Either you modify only the drcov class to cover this case and let the QlLoaderBLOB unchanged. As this is a very particular use case I think the second would be the best.
I am free to try to make the modifications if needed :)
I also forgot to mention this but maybe an improvement could be done in the way the covered blocks (or instructions) are stored. For now, each time a block is hit, it's added to this list : self.basic_blocks via the callback and the bb_entry class. That means that if the same block is hit twice or more it's added multiple times in the list and consequently in the coverage file. In the drcov format this is useless because there is no measure for multiple time hit block or instruction. This list storage leads to two drawbacks : first one the list stored in memory could be way larger than it should be and logically the coverage file is larger as well. I suggest maybe to use a dictionary instead of a list (don't know if all of this is relevant in performance terms but I've seen this so I just point it out). If using a dictionary, the data used for the key should be carefully chosen because of module id. (This comment and suggestion is only valid for the drcov format, I don't know the others)
Hi there and thanks for reporting this. Let me address the concerns you brought up:
- There are no items in
loader.imageswhen running as Blob: it might make sense to register the entire blob as a single image. Though it is not strictly an image, it does work around this problem in an elegant way - Address not being recognized on IDA: I guess this has something to do with the base address set in IDA. Reading the code shows that we store the basic blocks' offset from image base, not their address. If that image is loaded to address 0 then offset equals address, but not on any other case. The solution to that is to load the blob both on Qiling and IDA to the same base address
- Meaningless record duplication: I guess a
setis more adequate here, butdictcould work too
Here is a quick fix; you are welcome to pull it and let me know if it works for you: https://github.com/elicn/qiling/tree/fix-blob-coverage
Hey ! Thanks for your answer !
- Yeah, add the images attribute in the blob class was the fix I adopted in my setup.
- I tried your fix, problem 1 is fixed but I still have the problem 2 I talked about. I think I haven't been enough clear in my previous post. If my binary works with physical address for example is supposed to be loaded from 0xffff0000 to 0xffff7fff. I have loaded it in this range in Qiling via a cortex_a.ql using entry_point and ram_size attributes :
[CODE]
ram_size = 0x8000
entry_point = 0xFFFF0000
heap_size = 0x300000
[MISC]
current_path = /
I've also loaded the binary in IDA to match this address range. But when performing the substract we are recording instructions with address that will be outside of this range eg instruction stored at 0xffff0000 will be saved as (0xffff0000 - loader.images.img_base) = 0 and doesn't match the rest. I think I am missing something or doing something wrong. Don't hesitate to correct my setup but as soon as both binaries are loaded at same range address in both environment, I don't get the use of the substract.
If my fixes solves the first problem (and hopefully speeds up coverage), then we can move on to merge it. I am not familiar with those coverage tools so I really can't say what is the right approach here. I don't know whether absolute addresses or relative offsets should be stored. Qiling currently stores offsets; maybe IDA (or the tool that processes the coverage info) needs to be configured to treat those values as offsets and not absolute addresses.