Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

gdb prints invalid address of static const arrays of non-string values for classes with virtual functions

Tags:

c++

gdb

EDIT: Please scroll down to the "EDIT" section at the end of the question for more recent details. I'm not editing the rest of this post to preserve history of the comments.

I have a class defined like so in a header file:

class TestClass
{
public:
  TestClass() { }
  ~TestClass() { }
  void Test();
private:
  static const char * const carr[];
  static const int iarr[];
};

The TestClass::Test() function just makes sure that both arrays are used so they're not optimized away - prints them to log. I won't post it here for clarity. Arrays are initialized in .cpp file.

Above case works fine, when creating an instance of this class, the addresses look like so:

t   TestClass * 0x20000268  
    carr    const char * const[]    0x8002490 <TestClass::carr> 
    iarr    const int []    0x800249c <TestClass::iarr>

Memory addresses beginning with 0x20... belong to RAM region, while 0x80... belong to ROM/Flash. As expected, both arrays get placed in ROM.

However if I add virtual qualifier to any function in the class, e.g. its destructor like so:

class TestClass
{
public:
  TestClass() { }
  virtual ~TestClass() { }
  void Test();
private:
  static const char * const carr[];
  static const int iarr[];
};

Then the result is this:

t   TestClass * 0x20000268  
    carr    const char * const[3]   0x80024b4 <TestClass::carr> 
    iarr    const int [1000]    0x20000270

In particular - iarr is placed in RAM which is totally not what I expected.

This file is compiled like so:

arm-none-eabi-g++ -mcpu=cortex-m7 -mthumb -mfloat-abi=soft -O0 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -ffreestanding -fno-move-loop-invariants -Wall -Wextra -g3 -DDEBUG -DUSE_FULL_ASSERT -DTRACE -DOS_USE_TRACE_ITM -DSTM32F767xx -DUSE_HAL_DRIVER -DHSE_VALUE=24000000 -I../include -I../system/include -I../system/include/cmsis -I../system/include/stm32f7-hal -std=gnu++11 -fabi-version=0 -fno-exceptions -fno-rtti -fno-use-cxa-atexit -fno-threadsafe-statics -c -o "src\\main.o" "..\\src\\main.cpp" 

And the linking part:

arm-none-eabi-g++ -mcpu=cortex-m7 -mthumb -mfloat-abi=soft -O0 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -ffreestanding -fno-move-loop-invariants -Wall -Wextra  -g3 -T mem.ld -T libs.ld -T sections.ld -nostartfiles -Xlinker --gc-sections -L"../ldscripts" -Wl,-Map,"VirtualClassTestF7.map" --specs=nano.specs -o "VirtualClassTestF7.elf" "@objs.rsp"  

There are more files built in this project, related to hardware initialization. I don't include those to keep the post short.

Is there any switch that controls this behavior? I've already tried the obvious parts that I could come up with, which could have a slightest connection with the issue:

  • Optimization levels: O0, O1, O2, O3, Os, Ofast
  • Removing -ffunction-sections and -fdata-sections
  • Adding -fno-common
  • Making the array larger to go over some threshold if there was some. I've made its size 10k elements (times sizeof(uint32_t)) and it was still in RAM
  • Trying three different versions of toolchain

Toolchain is arm-none-eabi. Tried versions (outputs of arm-none-eabi-gcc --version):

  • arm-none-eabi-gcc.exe (GNU Tools for ARM Embedded Processors) 4.9.3 20150529 (release) [ARM/embedded-4_9-branch revision 224288]
  • arm-none-eabi-gcc.exe (bleeding-edge-toolchain) 7.2.0
  • arm-none-eabi-gcc.exe (bleeding-edge-toolchain) 8.3.0
  • Cygwin (gcc (GCC) 7.4.0)

First one comes from official ARM website: https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads. Last two come from http://www.freddiechopin.info/en/download/category/11-bleeding-edge-toolchain as ARM officially doesn't release 64-bit version and our project grew to size that breaks the 32-bit version.

Why this is an issue and why I'm specifically looking into a compiler switch: Possibly there is another way to force those values into ROM by writing it a bit differently. This is not an option - we've encountered this issue recently in a larger project spanning thousands of files where class inheritance is used heavily in various places. Catching all the possible occurrences of such arrays (some being created with macros, some by external tools) and then reorganizing all that code is out of the question. Therefore I'm looking for a reason why the compiler behaves in this exact way and what are the possible solutions that don't involve touching the source files.

EDIT: It appears to be some kind of issue with gdb and how it retrieves the address of that variable, or I'm missing something. I went ahead and created the same example on PC (Cygwin gcc 7.4.0):

#include <stdio.h>

class TestClass
{
public:
  TestClass() { }
  virtual ~TestClass() { }
  static const char * const carr[];
  static const int iarr[];
};

const char * const TestClass::carr[] = {
    "test1", "test2", "test3"
};

const int TestClass::iarr[] = {
    1,2,3,4,5,6,7,8,9,0
};

int main() {
  TestClass instance;
  printf("instance: %p, carr: %p, iarr: %p\n", &instance, instance.carr, instance.iarr);
  fflush(stdout);
  while(1);
  return 0;
}

The output of the program is this:

instance: 0xffffcba8, carr: 0x100403020, iarr: 0x100403040

This is also confirmed by the map file. Relevant portion:

 .rdata         0x0000000100403000       0xa0 ./src/main.o
                0x0000000100403020                TestClass::carr
                0x0000000100403040                TestClass::iarr

However gdb shows this:

p instance.iarr
$2 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
p &instance.iarr
[New Thread 57872.0x4f28]
$3 = (const int (*)[10]) 0x60003b8a0
p &instance.iarr
$4 = (const int (*)[10]) 0x60003b8d0

And what's even more interesting is that this address changes every time I attempt to print it with gdb. What's the reason for this?

Question title and tags adjusted.

like image 471
Jacek Ślimok Avatar asked Mar 15 '19 13:03

Jacek Ślimok


1 Answers

gdb copies your array into RAM, you don't even need an instance for it, a class with a vtable is enough:

(gdb) p TestClass::iarr
$1 = {1, 2, 3, 4, 5, 6}
(gdb) p (int*)TestClass::iarr
$2 = (int *) 0x7ffff7a8b780
(gdb) p *(int *) 0x7ffff7a8b780 @ 100
$3 = {1, 2, 3, 4, 5, 6, 0 <repeats 94 times>}
(gdb) p (int*)TestClass::iarr
$4 = (int *) 0x7ffff7a8b7a0
(gdb) p (int*)TestClass::iarr
$5 = (int *) 0x7ffff7a8b7c0
(gdb) p *(int *) 0x7ffff7a8b780 @ 100
$6 = {1, 2, 3, 4, 5, 6, 0, 0, 1, 2, 3, 4, 5, 6, 0, 0, 1, 2, 3, 4, 5, 6, 0 <repeats 78 times>}

I guess this boils down to gdb's interpretation of "C". If you need the real address in gdb, you need a function that returns it.

like image 88
user1531083 Avatar answered Sep 29 '22 11:09

user1531083