为什么 System.map 中函数的地址在实时看到的地址之前有一个字节?
在Linux内核中,System.map是一个非常重要的文件,它记录了内核中各个函数以及全局变量的地址。然而,有一个奇怪的现象是,System.map中记录的函数地址与实际在内存中查看的地址之间存在一个字节的差异。为什么会出现这种情况呢?要理解这个问题,我们首先需要了解一些与内存对齐相关的概念。在计算机系统中,内存是以字节为单位进行寻址的,而不是以位或者其他更小的单位。因此,每个变量或者数据结构的地址都必须是字节对齐的,也就是说,它们的地址必须是某个特定值的倍数。在32位的x86架构中,一般情况下,变量或者数据结构的起始地址必须是4的倍数,也就是说,地址的最低两位必须为0。而在64位的x86架构中,地址的最低三位必须为0,也就是说,起始地址必须是8的倍数。内存对齐的原理内存对齐的原理在于提高内存的访问效率。当我们读取一个没有对齐的数据时,处理器需要进行两次内存访问才能正确地读取数据,这样会增加访问内存的时间。而如果数据是对齐的,处理器只需要一次内存访问就可以正确地读取数据,从而提高了处理速度。当我们定义一个函数时,编译器会自动进行内存对齐的操作。在32位的x86架构中,函数的起始地址必须是4的倍数,而在64位的x86架构中,函数的起始地址必须是8的倍数。因此,当我们查看System.map中记录的函数地址时,它们都是经过内存对齐后的地址。函数地址的偏移然而,在实际运行程序时,我们可能会发现System.map中记录的函数地址与实际在内存中看到的地址之间存在一个字节的差异。这是因为在执行函数调用时,处理器需要将指令的地址与函数的起始地址进行对齐,从而正确地执行函数。在函数调用的指令中,通常使用的是相对地址,也就是相对于当前指令地址的偏移量。因此,当处理器执行函数调用指令时,它会将当前指令的地址与函数的起始地址进行对齐,并将对齐后的地址作为函数调用的参数。这样,函数就可以正确地执行,并返回结果。因此,在实时调试程序时,我们看到的函数地址实际上是经过了偏移的地址。这个偏移量正好是一个字节,因为在32位的x86架构中,函数的起始地址必须是4的倍数,而在64位的x86架构中,函数的起始地址必须是8的倍数。案例代码为了更好地理解这个问题,我们可以通过一个简单的例子来演示。c#include void foo() { printf("Hello, World!\n");}int main() { foo(); return 0;}
我们将上述代码保存为example.c,并通过gcc编译器进行编译。bashgcc -o example example.c
然后,我们可以查看System.map文件中foo函数的地址。bashcat /proc/kallsyms | grep foo
在输出中,我们可以看到foo函数的地址。然后我们可以通过gdb调试工具来查看实际的函数地址。bashgdb example(gdb) p foo
在输出中,我们可以看到实际的函数地址。比较System.map中记录的地址和实际的地址,可以发现它们之间存在一个字节的差异。在Linux内核中,System.map文件记录的函数地址与实际在内存中查看的地址之间存在一个字节的差异。这是因为在执行函数调用时,处理器需要将指令的地址与函数的起始地址进行对齐,从而正确地执行函数。因此,在实时调试程序时,我们看到的函数地址实际上是经过了偏移的地址。这个偏移量正好是一个字节,因为在32位的x86架构中,函数的起始地址必须是4的倍数,而在64位的x86架构中,函数的起始地址必须是8的倍数。通过了解内存对齐的原理和函数地址的偏移,我们可以更好地理解System.map中函数地址与实际地址之间的差异。