使用GDB检查数据的内存布局

4
假设我们有以下简单的C++代码:
#include <iostream>

int main(){
  int a = 5;
}

由于每个存储单元大小为8位,而整数大小为32位,因此我认为a的内存结构应该是这样的:

0xa      0xb      0xc      0xd 
00000000 00000000 00000000 00000101

其中0xa、0xb、0xc、0xd是示例内存地址。

1) &a指向0xa还是0xd?

2) 如果我使用GDB并使用x获取实际的内存地址,我将得到以下结果:

(gdb) p a
$7 = 5
(gdb) p &a
$8 = (int *) 0x7ffeefbffac8
(gdb) x/bt 0x7ffeefbffac8
0x7ffeefbffac8: 00000101
(gdb) x/bt 0x7ffeefbffac8-1
0x7ffeefbffac7: 00000000
(gdb) x/bt 0x7ffeefbffac8-2
0x7ffeefbffac6: 00000000
(gdb) x/bt 0x7ffeefbffac8-3
0x7ffeefbffac5: 01111111
(gdb) 

为什么0x7ffeefbffac8-3的值是01111111而不是00000000?这个地址不是等于我们样例中的0xa吗?涉及到IT技术相关内容。


1
你很可能正在使用LittleEndian系统,因此字节被交换了,即最不重要的字节首先出现。 - tkausl
那么在LittleEndian指针&a指向0xd吗? - Joe
不,它指向第一个字节,只是字节在内存中被反转了。 - tkausl
每个对象都有一个地址,被指定为'&a'。如果该对象占用多个字节,则我的经验是额外的字节位于更高的地址。 - 2785528
2个回答

4

在小端机器上,&a指向内存中最不重要的字节。也就是说,如果&a == 0x7ffeefbffac8,那么a就存在于字节中。

0x7ffeefbffac8:  101   << least significant byte
0x7ffeefbffac9:  000
0x7ffeefbffaca:  000
0x7ffeefbffacb:  000   << most significant byte.

通过将例如0x0a090705分配给a来观察最佳效果,然后执行以下操作:

Temporary breakpoint 1, main (argc=3, argv=0x7fffffffdc68) at t.c:2
2     int a = 0x0a090705;
(gdb) n
3     return 0;
(gdb) p &a
$1 = (int *) 0x7fffffffdb7c

检查从&a开始的4个字节:

(gdb) x/4bt 0x7fffffffdb7c
0x7fffffffdb7c: 00000101    00000111    00001001    00001010

或者,等价地,可以逐字节进行:
(gdb) x/bt 0x7fffffffdb7c
0x7fffffffdb7c: 00000101
(gdb) x/bt 0x7fffffffdb7c+1
0x7fffffffdb7d: 00000111
(gdb) x/bt 0x7fffffffdb7c+2
0x7fffffffdb7e: 00001001
(gdb) x/bt 0x7fffffffdb7c+3
0x7fffffffdb7f: 00001010

为什么0x7ffeefbffac8-3填充为01111111而不是00000000
因为您走错了方向:&a-3根本不属于a(它属于其他东西,或者可能是未初始化的随机垃圾)。

0
2) 如果我使用GDB并使用x获取真实内存地址,我会得到以下结果:

在大多数桌面电脑和特别是Linux上,显示的地址是虚拟地址,而不是“真实”(实际)地址。

在嵌入式工具套件(如vxWorks)中,即使使用虚拟内存,调试器也可以显示硬件地址和值。

注意:我尚未在具有实际硬件地址的系统上使用任何形式的Linux进行访问,但我已经在嵌入式软件上使用g++和gdb。


1) &a 指向的是 0xa 还是 0xd?

C++ 代码片段可以展示 int 和 byte 地址以及十六进制或十进制的值。

     int a = 0x0d0c0b0a;
     //  msB---^^    ^^---lsB

     char* a0 = reinterpret_cast<char*>(&a);
     char* a1 = a0+1;
     char* a2 = a0+2;
     char* a3 = a0+3;

     cout  //              Note: vvvvvvvvvvvvv---improves readability 
        << "\n  value of a: " << sop.digiComma(to_string(a))
        << "\n  sizeof(a):  " << sizeof(a) << " bytes   "
        << "\n  address:    " << &a << '\n'
        << "\n  hex value:  " << "0x" << hex << setfill('0') << setw(8) << a << hex
        //
        << "\n              " <<                                   "   | | | |"
        << "\n         a0:  " << setw(2) << static_cast<int>(*a0) << " | | |-^ lsB  " << static_cast<void*>(a0)
        << "\n         a1:  " << setw(2) << static_cast<int>(*a1) << " | |-^        " << static_cast<void*>(a1)
        << "\n         a2:  " << setw(2) << static_cast<int>(*a2) << " |-^          " << static_cast<void*>(a2)
        << "\n         a3:  " << setw(2) << static_cast<int>(*a3) << "-^       msB  " << static_cast<void*>(a3)
        << endl;

典型输出:(位置可能会改变)

value of a: 218,893,066
sizeof(a):  4 bytes   
address:    0x7ffee713c1dc

  hex value:  0x0d0c0b0a
                 | | | |
         a0:  0a | | |-^ lsB  0x7ffee713c1dc
         a1:  0b | |-^        0x7ffee713c1dd
         a2:  0c |-^          0x7ffee713c1de
         a3:  0d-^       msB  0x7ffee713c1df

为什么0x7ffeefbffac8-3填充了01111111而不是00000000?这个地址不等于我们示例内存地址中的0xa吗?
另一个答案(涉及到-3)说:“你走错方向了”,我同意。对我来说,这只是您对对象在内存中“布局”的误解。
这说明了所有调试器的问题...成功的用户必须知道编译器如何做事情,如何在内存中“布局”简单对象。我编写的代码片段使用简单的c++代码展示了一种让编译器展示其布局选择的方法。
总结:
您可以轻松添加诊断程序以显示内存布局进行检查和内容审查,每个程序都使用c++(或必要时使用c-style)的舒适功能。
您可以轻松让调试器报告对象的当前地址。
因此,您可能考虑将这两个想法结合起来:
a)我经常创建类似上面的说明性代码片段,以简单的文本形式展示我要确认或审查的对象的编译器内存布局。请注意,更改编译器选项可能会更改布局选择。 b) 除此之外,我还创建了一个简短的访问函数,可在调试器命令行上调用。该访问函数会调用示例代码。 c) 在如何使函数调用说明性代码方面可能会遇到一些挑战,但软件非常灵活,我没有遇到任何问题。 d) 有时,我发现将对象地址传递到函数中(作为命令行的一部分)更容易。其他时候,则单个地址被隐含地使用。 e) 通常,访问函数是唯一调用说明性代码的代码,因此两者都从操作代码中剔除。即它们不会对正常操作产生影响(因此很容易删除)。

“你可以轻松添加诊断例程” - 是的,你可以,但这个问题是关于使用GDB的。而“虚拟与实际”的区别与问题几乎没有任何关系。 - Employed Russian
@EmployedRussian - 谢谢提醒。生活打断了我。 - 2785528

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接