I can only provide an example for this for Raspberry Pi Pico - therefore ARM - but I hope this question can also be answered generally in terms of gcc and .elf files.
When programming for the Pico, the pico-sdk contains macros like bi_decl
and bi_1pin_with_name
, which allow for embedding of information in executables, which can then be read with a PC program called picotool
. My question is: can I (somehow) relatively easily read this data with binutils
tools (such as readelf
, objdump
, etc)?
Here is an example - a Pico project with some trivial code, consisting of:
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.12)
include(pico_sdk_import.cmake)
project(test_bi)
pico_sdk_init()
add_executable(test_bi test_bi.c)
target_link_libraries(test_bi pico_stdlib)
pico_add_extra_outputs(test_bi)
test_bi.c
#include <stdint.h>
#include "pico/binary_info.h"
bi_decl(bi_1pin_with_name(PICO_DEFAULT_SPI_CSN_PIN, "MYPINNAME"));
int main(void) {
while ( 1 ) {
;
}
}
When I build this, I get a test_bi.elf
binary executable - and when I inspect that executable with picotool
on the PC, I get:
$ picotool info -a test_bi.elf
File test_bi.elf:
Program Information
name: test_bi
binary start: 0x10000000
binary end: 0x10005224
Fixed Pin Information
17: MYPINNAME
Build Information
sdk version: 1.4.0
pico_board: pico
boot2_name: boot2_w25q080
build date: Oct 21 2022
build attributes: Debug
So this confirms, that the information entered as bi_decl(bi_1pin_with_name(PICO_DEFAULT_SPI_CSN_PIN, "MYPINNAME"))
in code, ended up in the .elf
executable. (picotool likely used this case in the visit
function to do the reading).
However, I cannot see "MYPINNAME" in my attempts to use readelf
:
$ arm-none-eabi-readelf -a -W -n test_bi.elf | grep MYPIN
$
... but I can see it with strings
-- which, when called with -t x
, also gives relative offset address:
$ arm-none-eabi-strings -t x test_bi.elf | grep MYPIN
4dff MYPINNAME
... but I don't really understand how I could further look up this address in the .elf - just mere grepping of readelf
output does not seem to help much:
$ arm-none-eabi-readelf -a -W -n test_bi.elf | grep 4d
259: 100044d8 0 NOTYPE LOCAL DEFAULT 3 $d
260: 100044d8 19 OBJECT LOCAL DEFAULT 3 __func__.1
267: 100014da 0 NOTYPE LOCAL DEFAULT 2 $t
471: 100034d6 0 NOTYPE LOCAL DEFAULT 2 l12_1
543: 10003a4d 68 FUNC LOCAL DEFAULT 2 stdio_stack_buffer_flush
733: 100014db 30 FUNC GLOBAL DEFAULT 2 recursive_mutex_init
It's likely the relative address 4dff would map to absolute address 10004dff, but it seems I can't use it for much: trying objdump
with a relative address range returns three different sections, none of which contain the string - and with an absolute address range, nothing is returned:
$ arm-none-eabi-objdump -S -s --start-address 0x4df0 --stop-address 0x4e10 test_bi.elf
test_bi.elf: file format elf32-littlearm
Contents of section .debug_info:
4df0 32000038 390f0000 01b23f02 01000080 2..89.....?.....
4e00 3200007a 3200003a 3f0d0000 01b31381 2..z2..:?.......
Contents of section .debug_line:
4df0 01050516 051c034f 01050513 03f9002e .......O........
4e00 050b0601 2e050506 03290105 1c03a77f .........)......
Contents of section .debug_loc:
4df0 0e00103c 0f001001 00563e0f 0010780f ...<.....V>...x.
4e00 00100100 56000000 00000000 000001f2 ....V...........
$ arm-none-eabi-objdump -S -s --start-address 0x10004df0 --stop-address 0x10004e10 test_bi.elf
test_bi.elf: file format elf32-littlearm
To get closer to this, I compiled the project with make VERBOSE=1
, extracted the gcc
compilation line, added -E
to it so only preprocessor output is generated - and I arrived to this equivalent of the above .c file, where the bi_decl(bi_1pin_with_name(PICO_DEFAULT_SPI_CSN_PIN, "MYPINNAME"))
statement is expanded:
test_pi_exp.c
:
#include <stdint.h>
typedef unsigned int uint;
typedef struct __attribute__((__packed__)) _binary_info_core {
uint16_t type;
uint16_t tag;
} binary_info_core_t;
typedef struct __attribute__((__packed__)) _binary_info_pins_with_name {
struct _binary_info_core core;
uint32_t pin_mask;
const char * label;
} binary_info_pins_with_name_t;
static const __attribute__((__unused__)) int _error_bi_is_missing_enclosing_decl_4=0;
static const struct _binary_info_pins_with_name __bi_4 = {
.core = {
.type = (9 + _error_bi_is_missing_enclosing_decl_4),
.tag = ((((uint)'P'&0xffu)<<8u)|((uint)'R'&0xffu)),
},
.pin_mask = (1u << (17)),
.label = ("MYPINNAME") };
static const __attribute__((__used__)) __attribute__((section(".binary_info.keep." "__bi_ptr4"))) struct _binary_info_core *__bi_ptr4 = &__bi_4.core;;
int main(void) {
while ( 1 ) {
;
}
}
We can compile this without the pico-sdk with arm-none-eabi-gcc -g --specs=nosys.specs test_bi_exp.c -o test_bi_exp.elf
.
Anyways, now we can tell the bi_decl...
statement ended up as symbols prefixed with __bi_
, here __bi_4
and __bi_ptr4
; since it's not obvious to me exactly which symbol ended up with the label text content of the bi_decl
, I'd say that the content cannot be obtained directly from these symbols (and that is why I used "indirect reading" in the question title).
Let's see how our executables compare in terms of __bi_
symbols:
$ arm-none-eabi-readelf -a -W -n test_bi.elf | grep __bi
49: 10004c24 4 OBJECT LOCAL DEFAULT 4 __bi_ptr22
50: 100002b4 12 OBJECT LOCAL DEFAULT 2 __bi_22
52: 10004c28 4 OBJECT LOCAL DEFAULT 4 __bi_ptr30
53: 100002a8 12 OBJECT LOCAL DEFAULT 2 __bi_30
55: 10004c2c 4 OBJECT LOCAL DEFAULT 4 __bi_ptr38
56: 1000029c 12 OBJECT LOCAL DEFAULT 2 __bi_38
58: 10004c30 4 OBJECT LOCAL DEFAULT 4 __bi_ptr44
59: 10004bf4 12 OBJECT LOCAL DEFAULT 3 __bi_44
61: 10004c34 4 OBJECT LOCAL DEFAULT 4 __bi_ptr50
62: 10000290 12 OBJECT LOCAL DEFAULT 2 __bi_50
64: 10004c38 4 OBJECT LOCAL DEFAULT 4 __bi_ptr75
65: 10004c00 12 OBJECT LOCAL DEFAULT 3 __bi_75
67: 10004c3c 4 OBJECT LOCAL DEFAULT 4 __bi_ptr81
68: 10004c0c 12 OBJECT LOCAL DEFAULT 3 __bi_81
89: 10004c20 4 OBJECT LOCAL DEFAULT 4 __bi_ptr4
90: 10003e0c 12 OBJECT LOCAL DEFAULT 3 __bi_4
645: 10004c40 0 NOTYPE GLOBAL DEFAULT 4 __binary_info_end
744: 10004c20 0 NOTYPE GLOBAL DEFAULT 4 __binary_info_start
776: 100001e8 0 NOTYPE GLOBAL DEFAULT 2 __binary_info_header_end
$ arm-none-eabi-readelf -a -W -n test_bi_exp.elf | grep __bi
[10] .binary_info.keep.__bi_ptr4 PROGBITS 00018ac4 008ac4 000004 00 WA 0 0 4
02 .init_array .fini_array .data .binary_info.keep.__bi_ptr4 .bss
10: 00018ac4 0 SECTION LOCAL DEFAULT 10 .binary_info.keep.__bi_ptr4
60: 00008660 12 OBJECT LOCAL DEFAULT 4 __bi_4
62: 00018ac4 4 OBJECT LOCAL DEFAULT 10 __bi_ptr4
The takeout here for me is:
- the pico-sdk compiled .elf executable has several
__bi_*
entries (which correspond to other entries shown bypicotool
above); however there are no.binary_info.keep.__bi_*
sections - the "plain" .elf executable still keeps
.binary_info.keep.__bi_*
sections, and has only the one__bi_4
entry that we specified in the file
Maybe if the pico-sdk
.elf kept the .binary_info.keep.__bi_*
sections things may have been easier - but given that they are not, let me restate what I want to do:
Even if I have to go through the extra gcc -E
step, to find that a given bi_decl...
statement ended as (a) given symbol(s) (say __bi_4
) - could I use binutils tools to: start from symbol(s) name(s) like __bi_4
; then obtain addresses that this/these symbol(s) "cover(s)"; and then obtain a binary hexdump of that address range, that would show (and confirm) that the text I used in source code (here "MYPINNAME"), did indeed end up at those locations? And if so - how do I that?