[PATCH] Fix tekhex format related gdb.base/dump.exp failures
Simon Marchi
simark@simark.ca
Mon Aug 25 15:43:46 GMT 2025
On 8/24/25 2:09 AM, Kevin Buettner wrote:
> [ Re-sent to both binutils and gdb-patches. ]
>
> On s390x, a big-endian machine, I'm seeing these test failures:
>
> FAIL: gdb.base/dump.exp: array as memory, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: array as memory, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: array as value, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: array as value, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: array copy, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: array copy, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: array partial, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: array partial, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: dump array as memory, tekhex
> FAIL: gdb.base/dump.exp: dump array as value, tekhex
> FAIL: gdb.base/dump.exp: dump struct as memory, tekhex
> FAIL: gdb.base/dump.exp: dump struct as value, tekhex
> FAIL: gdb.base/dump.exp: reload array as memory, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: reload array as value, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: reload struct as memory, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: reload struct as value, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: struct as memory, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: struct as memory, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: struct as value, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: struct as value, tekhex; value restored ok
> FAIL: gdb.base/dump.exp: struct copy, tekhex; file restored ok
> FAIL: gdb.base/dump.exp: struct copy, tekhex; value restored ok
>
> It turns out that there's a subtle bug in move_section_contents in
> bfd/tekhex.c. The bug is that when attempting to write a buffer that
> starts with a zero byte, the function will return false, an error
> condition, without writing anything. But it also doesn't set
> bfd_error, so GDB ends up displaying whatever the last unrelated error
> was, e.g.:
>
> warning: writing dump file '.../intstr1.tekhex' (No such file or directory)
>
> When I investigated this, the bfd error was set during failure to
> open a separate debug file for the test case, which is totally
> unrelated to this problem.
>
> The reason this fails on big endian machines is that the test case
> writes out structs and arrays of int initialized to small values. On
> little endian machines, the small integer is the first byte, so the
> error doesn't occur. On big endian machines, a zero byte occurs
> first, triggering the error.
>
> On the GDB side of things, I've made a one line change to the test
> case to cause the error to also happen on little endian machines. I
> simply shift value of the first field in the struct left by 16 bits.
> That leaves at least one zero byte on both sides of the non-zero part
> of the int. I shifted it by 16 because, for a moment, there was a
> question in my mind about what would happen with a second zero byte,
> but it turns out that it's not a problem.
>
> On the bfd side of things, take a look at move_section_contents() and
> find_chunk() in tekhex.c. The scenario is this: we enter
> move_section_contents with locationp pointing at a character buffer
> whose first byte is zero. The 'get' parameter is false, i.e. we're
> writing, not reading. The other critical fact is that the
> abfd->tdata.tekhex_data->data is NULL (0).
>
> I'm going to go through the execution path pretty much line by line
> with commentary below the line(s) just executed.
>
> char *location = (char *) locationp;
> bfd_vma prev_number = 1; /* Nothing can have this as a high bit. */
>
> I can't say that the comment provides the best explanation about
> what's happening, but the gist is this: later on, chunk_number will
> have it's low bits masked away, therefore no matter what it is, it
> can't possibly be equal to prev_number when it's set to 1.
>
> struct data_struct *d = NULL;
>
> BFD_ASSERT (offset == 0);
> for (addr = section->vma; count != 0; count--, addr++)
> {
>
> Set d to NULL and enter the loop.
>
> /* Get high bits of address. */
> bfd_vma chunk_number = addr & ~(bfd_vma) CHUNK_MASK;
> bfd_vma low_bits = addr & CHUNK_MASK;
>
> Use CHUNK_MASK, which is 0x1fff, to obtain the chunk number, i.e.
> whatever's left after masking off the low 13 bits of addr, and
> low_bits, which are the low 13 bits of addr. chunk_number matters for
> understanding this bug, low_bits does not. Remember that no matter
> what addr is, once you mask off the low 13 bits, it can't be equal to 1.
>
> bool must_write = !get && *location != 0;
>
> !get is true, *location != 0 is false, therefore the conjunction is
> false, and furthermore must_write is false. I.e. even though we are
> writing, we don't transfer zero bytes to the chunk - this is why
> must_write is false. (The reason this works is that a chunk, once
> allocated, is zero'd as part of the allocation using bfd_zalloc.
> Therefore we can skip transferring zero bytes and, if enough of them
> are skipped one after another, chunk allocation simply doesn't happen.
> That's a good thing.)
>
> if (chunk_number != prev_number || (!d && must_write))
>
> For the reason provided above, chunk_number != prev_number is true.
> The other part of the disjunction doesn't matter since the first part
> is true. This means that the if-block is entered.
>
> /* Different chunk, so move pointer. */
> d = find_chunk (abfd, chunk_number, must_write);
>
> find_chunk is entered with must_write set to false. Now, remember
> where we left off here, because we're going to switch to find_chunk.
>
> static struct data_struct *
> find_chunk (bfd *abfd, bfd_vma vma, bool create)
> {
>
> (Above 3 lines indented to distinguish code from commentary.)
>
> When we enter find_chunk, create is false because must_write was false.
>
> struct data_struct *d = abfd->tdata.tekhex_data->data;
>
> d is set to NULL since abfd->tdata.texhex_data->data is NULL (one of
> the conditions for the scenario).
>
> vma &= ~CHUNK_MASK;
> while (d && (d->vma) != vma)
> d = d->next;
>
> d is NULL, so the while loop doesn't execute.
>
> if (!d && create)
> ...
>
> d is NULL so !d is true, but create is false, so the condition
> evaluates to false, meaning that the if-block is skipped.
>
> return d;
>
> find_chunk returns NULL, since d is NULL.
>
> Back in move_section_contents:
>
> if (!d)
> return false;
>
> d is NULL (because that's what find_chunk returned), so
> move_section_contents returns false at this point.
>
> Note that find_section_contents has allocated no memory, nor even
> tried to transfer any bytes beyond the first (zero) byte. This
> is a bug.
>
> The key to understanding this bug is to observe that find_chunk can
> return NULL to indicate that no chunk was found. This is especially
> important for the read (get=true) case. But it can also be NULL
> to indicate a memory allocation error. I toyed around with the
> idea of using a different value to distinguish these cases, i.e.
> something like (struct data_struct *) -1, but although bfd contains
> plenty of code where -1 is used to indicate various interesting
> conditions for scalars, there's no prior art where this is done
> for a pointer. Therefore the idea was discarded in favor of
> modifying this statement:
>
> if (!d)
> return false;
>
> to:
> if (!d && must_write)
> return false;
>
> This works because, in find_chunk, the only way to return a NULL
> memory allocation error is for must_write / create to be true. When
> it is true, if bfd_zalloc successfully allocates a chunk, then that
> (non-NULL) chunk will be returned at the end of the function. When it
> fails, it'll return NULL early. The point is that when bfd_zalloc()
> fails and returns NULL, must_write (in move_section_contents) / create
> (in find_chunk) HAD to be true. That provides us with an easy test
> back in move_section_contents to distinguish a memory-allocation-NULL
> from a block-not-found-NULL.
>
> The other NULL return case happens when the end of the function is
> reached when either searching for a chunk to read or attempting to
> find a chunk to write when abfd->tdata.tekhex_data->data is NULL. But
> for the latter case, must_write was false, which does not (now, with
> the above fix) trigger the early return of false.
The GDB part LGTM.
Approved-By: Simon Marchi <simon.marchi@efficios.com> (GDB)
Simon
More information about the Binutils
mailing list