Update
Note that this technique finds each individual stateful variable in a library.
Thinking about this more, a simpler technique would be to simply copy the whole
.bss
and .data
sections. This is much simpler, requires many fewer copies,
and does not need DWARF. You end up copying more data since you copy the
padding bytes also, but this is likely still faster, since you only make 2 large
transfers. Another potential advantage of doing it this way, is that there might
be some data somewhere that would not show up in the DWARF: some language
runtime metadata or god-knows-what internal data in something. In my one
non-toy-problem test, the DWARF identified about 1600 variables for a grand
total of about 27MB. In any case, the DWARF logic still has its place, in case
we need finer-grained control than just dumping all the memory in a DSO.
See get_writeable_memory_ranges()
in the sources for an example implementation
of getting the relevant memory ranges..
Also note, that I found a bug related to the size reporting of multi-dimensional array: https://sourceware.org/bugzilla/show_bug.cgi?id=22546
Recently I needed to integrate a particular C library into a larger project I'm working on. This larger project has an "undo" feature, which requires that any action of the library must be revertable. This library does no I/O, so an undo sequence is
- save the persistent state of the library (all variables in memory)
- perform some library action
- revert to the previous state by restoring the state saved in 1.
The difficulty of this depends on the implementation details of each specific library. The easiest to handle are libraries that store all of their state in a context structure that is owned by the caller and that don't perform any dynamic memory allocation:
// inside library struct library_context_t { ... }; void library_do_thing (struct library_context_t* ctx); // inside client that invokes the library void call_library(struct library_context_t* ctx) { struct library_context_t ctx_saved = *ctx; library_do_thing(ctx); if(need to revert) *ctx = ctx_saved; }
The most effortful are libraries that have a lot of internal state they manage themselves and/or use dynamic allocation heavily.
My library didn't allocate anything dynamically, but it used internal state heavily. Things like this were strewn all over:
// library.c static int a; int b; extern struct { int w,x,y,z; } s; void library_do_thing(void) { static int c = 5; static struct { double v[10]; } d; a = ....; b = ....; }
So we need to deal with lots and lots of internal state. I had access to the
source, so the thing could be cleaned up to consolidate the hundreds of stateful
variables into a more manageable (and accessible, unlike the static
locals for
instance) number of structures. But this would require deep surgery into the
sources that would not be accepted upstream.
Fundamentally, the task is to save a set of areas in memory, each area having an
address and a size. This is mostly known at compile time, and some
instrumentation can be added in a save()
function in each source file in the
library:
// library.c static int a; int b; extern struct { int w,x,y,z; } s; void library_do_thing(void) { static int c = 5; static struct { double v[10]; } d; } void save(int* a_saved, int* b_saved) { *a_saved = a; *b_saved = b; // cannot access c and d }
This lets me save the file-global state, but not the function-local state. And I'd need to have one of these in each source file I'm instrumenting. So this doesn't fully work. I need to have something that can get the list of addresses and sizes at runtime.
The addresses are available from the symbol table, which I can get from the ELF
file I'm running (or linking with). With library.c
as above:
$ gcc -c -o library.o library.c $ nm library.o | awk '$2 ~ /^[bcdgsBCDGS]$/' 0000000000000000 b a 0000000000000004 C b 0000000000000000 d c.1766 0000000000000020 b d.1769
Here I only look at data and only data that is writeable because the
read-only data doesn't need to be saved/restored. We can see the global
variables a
and b
, and the static function-local variables c
and d
. And
we can see their addresses. We cannot, however see the sizes: these are known
and used by the compiler, which discards them when finished. I need another
source of data, and I found the perfect one: DWARF debugging information. This
contains much meta-data about the compiled code, including the location and
sizes of all the variables. DWARF info isn't necessarily available at runtime,
but I control the build flags, so I can make sure I have this. Example:
$ gcc -c -g -o library.o library.c $ readelf -wi library.o ... <1><3c>: Abbrev Number: 3 (DW_TAG_base_type) <3d> DW_AT_byte_size : 4 <3e> DW_AT_encoding : 5 (signed) <3f> DW_AT_name : int <1><43>: Abbrev Number: 4 (DW_TAG_variable) <44> DW_AT_name : b <46> DW_AT_decl_file : 1 <47> DW_AT_decl_line : 2 <48> DW_AT_type : <0x3c> <4c> DW_AT_external : 1 <4c> DW_AT_location : 9 byte block: 3 4 0 0 0 0 0 0 0 (DW_OP_addr: 4) ...
In this snippet we can see the variable b
and we can see that it is a 4-byte
integer at some address within the file.
I wrote a program to automatically parse this information and to generate the list of addresses and sizes that I care about. This is available at
https://github.com/dkogan/FindGlobals
The README is copied here:
Overview
This is a proof-of-concept program that is able to find all global state in a program by parsing its debug information (which is assumed to be available).
The global state I'm looking for is all persistent data:
- globals variables (static or otherwise)
- static local variables in functions
I'm only interested in data that can change, so I look only at the writeable segment. This program is alpha-quality, but passes some basic tests.
Example
tst1.c
:
// .... struct S1 v1[10]; volatile const char volatile_const_char; const volatile char const_volatile_char; const char const_char = 1; void print_tst1(void) { showvar(v1); showvar(volatile_const_char); showvar(const_volatile_char); showvar_readonly(const_char); }
tst2.c
:
// .... int v2 = 5; const char* const_char_pointer = NULL; const char const* const_char_const_pointer = NULL; char const* char_const_pointer = NULL; const char const_string_array[] = "456"; static const char static_const_string_array[] = "abc"; struct S0 s0; int x0[0]; int x1[30]; void print_tst2(void) { static int static_int_5[5]; static double static_double; double dynamic_d; showvar(static_int_5); showvar(static_double); showvar(v2); showvar(const_char_pointer); showvar_readonly(const_char_const_pointer); showvar_readonly(char_const_pointer); showvar_readonly(const_string_array); showvar_readonly(static_const_string_array); showvar(s0); showvar(x0); showvar(x1); }
main.c
:
// ... #include "getglobals.h" int main(int argc, char* argv[]) { printf("=================== Program sees: ===================\n"); print_tst1(); print_tst2(); printf("=================== DWARF sees: ===================\n"); get_addrs((void (*)())&print_tst1, "tst"); return 0; }
Results:
$ ./getglobals 2>/dev/null =================== Program sees: =================== v1 at 0x55756bc8e240, size 80 volatile_const_char at 0x55756bc8e220, size 1 const_volatile_char at 0x55756bc8e290, size 1 readonly const_char at 0x55756ba8cc65, size 1 static_int_5 at 0x55756bc8e040, size 20 static_double at 0x55756bc8e038, size 8 v2 at 0x55756bc8e010, size 4 const_char_pointer at 0x55756bc8e030, size 8 readonly const_char_pointer_const at 0x55756ba8cbd0, size 8 readonly char_pointer_const at 0x55756ba8cbc8, size 8 readonly const_string_array at 0x55756ba8cbc4, size 4 readonly static_const_string_array at 0x55756ba8cbc0, size 4 s0 at 0x55756bc8e120, size 100 x0 at 0x55756bc8e184, size 0 x1 at 0x55756bc8e1a0, size 120 =================== DWARF sees: =================== v2 at 0x55756bc8e010, size 4 const_char_pointer at 0x55756bc8e030, size 8 readonly const_char_pointer_const at 0x55756ba8cbd0, size 8 readonly char_pointer_const at 0x55756ba8cbc8, size 8 readonly const_string_array at 0x55756ba8cbc4, size 4 readonly static_const_string_array at 0x55756ba8cbc0, size 4 s0 at 0x55756bc8e120, size 100 x1 at 0x55756bc8e1a0, size 120 static_int_5 at 0x55756bc8e040, size 20 static_double at 0x55756bc8e038, size 8 v1 at 0x55756bc8e240, size 80 volatile_const_char at 0x55756bc8e220, size 1 const_volatile_char at 0x55756bc8e290, size 1 readonly const_char at 0x55756ba8cc65, size 1 dima@shorty:$ ./getglobals 2>/dev/null | sort | uniq -c | sort 1 =================== DWARF sees: =================== 1 =================== Program sees: =================== 1 x0 at 0x55be5ba3d184, size 0 2 const_char_pointer at 0x55be5ba3d030, size 8 2 const_volatile_char at 0x55be5ba3d290, size 1 2 readonly char_pointer_const at 0x55be5b83bbc8, size 8 2 readonly const_char at 0x55be5b83bc65, size 1 2 readonly const_char_pointer_const at 0x55be5b83bbd0, size 8 2 readonly const_string_array at 0x55be5b83bbc4, size 4 2 readonly static_const_string_array at 0x55be5b83bbc0, size 4 2 s0 at 0x55be5ba3d120, size 100 2 static_double at 0x55be5ba3d038, size 8 2 static_int_5 at 0x55be5ba3d040, size 20 2 v1 at 0x55be5ba3d240, size 80 2 v2 at 0x55be5ba3d010, size 4 2 volatile_const_char at 0x55be5ba3d220, size 1 2 x1 at 0x55be5ba3d1a0, size 120
In the example above we see the test program report the locations and addreses of its data. Then the instrumentation reports the addresses and data that it sees. These should be identical (in theory). We then sort and count duplicate lines in the output. If the two sets of reports were identical, we should see all lines appear 2 times. We see this with 2 trivial exceptions:
- The lines containing
=====
are delimiters x0
has size 0, so the instrumentation doesn't even bother to report it
Notes
- The example in this tree works with both static linking (
getglobals
) and dynamic linking (getglobals_viaso
). - This is alpha quality proof-of-concept software. It's an excellent starting point if you need this functionality, but do thoroughly test it for your use case.
- This was written with C code in mind. I can imagine that C++ can produce persistent state in more ways. Again, test it thoroughly
- The libraries I'm using to parse the DWARF are woefully underdocumented, and I'm probably not doing everything 100% correctly. At the risk of repeating myself: test it thoroughly.
- For ELF objects linked into the executable normally (whether statically or
dynamically) this works. If we're doing something funny like loading
libraries ourselves with
libdl
, then it probably works too, but I'd test it before assuming.
This effort uncovered a bug in gcc:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78100
It turns out that gcc erroneously omits the sizing information if you have an
array object pre-declared as an extern
of unknown size (as was done in my
specific library). So if you have this source:
#include <stdio.h> extern int s[]; int s[] = { 1,2,3 }; int main(void) { printf("%zd\n", sizeof(s)); return 0; }
then the object produced by gcc <= 6.2 doesn't know that s
has 3 integers.
Copyright and License
Copyright 2016 Dima Kogan.
Released under an MIT-style license:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.