首页 > 其他分享 >聊一聊 .NET高级调试 中必知的符号表

聊一聊 .NET高级调试 中必知的符号表

时间:2024-01-13 14:38:16浏览次数:26  
标签:中必知 符号表 0000000000000000 聊一聊 func var main data


一:背景

1. 讲故事

在高级调试的旅行中,发现有不少人对符号表不是很清楚,其实简而言之符号表中记录着一些程序的生物特征,比如哪个地址是函数(签名信息),哪个地址是全局变量,静态变量,行号是多少,数据类型是什么 等等,目的就是辅助我们可视化的调试,如果没有这些辅助我们看到的都是一些无意义的汇编代码,逆向起来会非常困难,这一篇我们就来系统的聊一聊。

二:程序编译的四个阶段

1. 案例代码

要想理解符号表,首先需要理解 代码文件 是如何变成 可执行文件 的,即如下的四个阶段。

  • 预处理阶段
  • 编译阶段
  • 汇编阶段
  • 链接阶段

为了能够看到每一个阶段,用 gcc 的相关命令手工推进,并用 chatgpt 写一段测试代码,包含全局变量,静态变量,函数等信息。

#include <stdio.h>
#define PI 3.1415926

int global_var = 10;

void func() {

    static int static_var = 5;

    printf("global_var = %d, static_var = %d PI=%f\n", global_var, static_var,PI);

    global_var++;
    static_var++;
}

int main() {
    func();
    func();

    return 0;
}

接下来用 gcc --help 命令查看下需要使用的命令列表。

[root@localhost data]# gcc --help
Usage: gcc [options] file...
Options:
  -E                       Preprocess only; do not compile, assemble or link
  -S                       Compile only; do not assemble or link
  -c                       Compile and assemble, but do not link
  -o <file>                Place the output into <file>
  ...

2. 预编译阶段

预处理主要做的就是代码整合,比如将 #include 文件导入,将 #define 宏替换等等,接下来使用 gcc -E 进行预处理。

[root@localhost data]# gcc main.c -E -o main.i
[root@localhost data]# ls
main.c  main.i

聊一聊 .NET高级调试 中必知的符号表_.net

可以看到这个 main.c 文件已经膨胀到了 858 行了。

3. 编译阶段

前面阶段是把代码预处理好,接下来就是将C代码编译成汇编代码了,使用 gcc -S 即可。

[root@localhost data]# gcc main.c -S -o main.s -masm=intel
[root@localhost data]# ls
main.c  main.i  main.s

聊一聊 .NET高级调试 中必知的符号表_性能优化_02

从图中可以看到汇编代码中也有很多辅助信息,比如 global_var 是一个 @object 变量,类型为 int,在 .rodata 只读数据段中,目的就是给汇编阶段打辅助。

4. 汇编阶段

有了汇编代码之后,接下来就是将 汇编代码 转成 机器代码,这个阶段会产生二进制文件,并且会构建 section 信息以及符号表信息,可以使用 gcc -c 即可。

[root@localhost data]# gcc main.c -c -o main.o -masm=intel
[root@localhost data]# ls
main.c  main.i  main.o  main.s

二进制文件模式默认是不能可视化打开的,可以借助于 objdump 工具。

[root@localhost data]# objdump
  -h, --[section-]headers  Display the contents of the section headers
  -t, --syms               Display the contents of the symbol table(s)

[root@localhost data]# objdump -t main.o

main.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 main.c
0000000000000000 l    d  .text	0000000000000000 .text
0000000000000000 l    d  .data	0000000000000000 .data
0000000000000000 l    d  .bss	0000000000000000 .bss
0000000000000000 l    d  .rodata	0000000000000000 .rodata
0000000000000004 l     O .data	0000000000000004 static_var.2179
0000000000000000 l    d  .note.GNU-stack	0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame	0000000000000000 .eh_frame
0000000000000000 l    d  .comment	0000000000000000 .comment
0000000000000000 g     O .data	0000000000000004 global_var
0000000000000000 g     F .text	0000000000000058 func
0000000000000000         *UND*	0000000000000000 printf
0000000000000058 g     F .text	000000000000001f main

在上面的符号表中看到了 func函数以及 static_varglobal_var 以及所属的 section。

5. 链接阶段

这个阶段主要是将多个二进制代码文件进一步整合变成可在操作系统上运行的可执行文件,可以使用 gcc -o

[root@localhost data]# gcc main.c -o main
[root@localhost data]# ls
main  main.c  main.i  main.o  main.s
[root@localhost data]# ./main
global_var = 10, static_var = 5 PI=3.141593
global_var = 11, static_var = 6 PI=3.141593
[root@localhost data]# objdump -t main

main:     file format elf64-x86-64

SYMBOL TABLE:
...
0000000000601034 g     O .data	0000000000000004              global_var
0000000000601034 g     O .data	0000000000000004              global_var
...
000000000040052d g     F .text	0000000000000058              func
...

相比汇编阶段,这个阶段的 符号表 中的第一列都是有地址值的,是相对模块的偏移值,比如说: module+0x000000000040052d 标记的是 func 函数。

上面是 linux 上的可执行文件的符号表信息,有些朋友说我是 windows 平台上的,怎么看符号表信息呢?

三:Windows 上的 pdb 解析

1. 观察 pdb 文件

上一节我们看到的是 linux 上 elf格式 的可执行文件,这一节看下 windows 平台上的PE文件 的符号表信息是什么样的呢?有了前面四阶段编译的理论基础,再聊就比较简单了。

在 windows 平台上 符号表信息 是藏在 pdb 文件中的,这种拆开的方式是有很大好处的,如果需要调试代码,windbg 会自动加载 pdb 文件,无调试的情况下就不需要加载 pdb 了,减少了可执行文件的大小,也提升了性能。

接下来用 SymView.exe 这种工具去打开 pdb 文件,截图如下:

聊一聊 .NET高级调试 中必知的符号表_windbg_03

从图中可以看到,符号表信息高达 10968 个,并且 func 函数的入口地址是在 module +0x11870 处,相当于做了一个标记,接下来我们拿这个func做一个测试。

2. 有 pdb 的 func 函数

首先说一下为什么通过 exe 可以找到 pdb,这是因为 PE 头的 DIRECTORY_ENTRY_DEBUG 节中记录了 pdb 的地址。

聊一聊 .NET高级调试 中必知的符号表_性能优化_04

只要这个路径有 pdb 就可以在 windbg 运行中按需加载了,然后通过 u MySample.exe+0x11870 观察,截图如下:

聊一聊 .NET高级调试 中必知的符号表_.net_05

图中显示的非常清楚,地址 00fd1870 就是 func 的入口地址,让一个无意义的地址马上有意义起来了,哈哈~~~

3. 无 pdb 的 func 函数

这一小节是提供给好奇的朋友的,如果没有 pdb,那汇编上又是一个什么模样,为了找到 func 的入口地址,我们内嵌一个 int 3 ,然后把 pdb 给删掉,代码如下:

int main() {

	__asm {
		int 3;
	}
	func();
	func();

	return 0;
}

聊一聊 .NET高级调试 中必知的符号表_开发语言_06

从图中可以看到,func 标记已经没有了,取而代之的都是 module+0xxx,这就会给我们逆向调试带来巨大的障碍。

三: 总结

总而言之,符号表就是对茫茫内存进行标记,就像百度地图一样,让我们知道某个经纬度上有什么建筑,让无情的地理坐标更加有温度,让世界更美好。


标签:中必知,符号表,0000000000000000,聊一聊,func,var,main,data
From: https://blog.51cto.com/u_15353947/9232344

相关文章

  • 聊一聊 .NET高级调试 中的一些内存术语
    一:背景1.讲故事在高级调试的旅程中,经常会有一些朋友问我什么是工作集(内存),什么是提交大小,什么是VirtualSize,什么是WorkingSet。。。截图如下:既然有很多朋友问,这些用口头也不怎么好描述,刚好上午有时间就系统的聊一下吧。二:内存术语解读1.VirtualSize是什么可能有些朋......
  • 聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(完结篇)
    前言书接上回,上回我们了解了castle代理的一些缺点,本文将开始操作整合Microsoft.Extension.Dependency和Castle,以让默认的容器可以支持拦截器我们将以进阶的形式逐步完善我们的封装,以实现一个更方便易用、普适、高性能的基础设施库。基础版还是先上代码,这是基础版本我们......
  • 聊一聊如何整合Microsoft.Extensions.DependencyInjection和Castle.Core(三)
    前言今天的第三篇,感觉没啥人看呀,难道没有兄弟跟我有同样的整合需求吗???手动,本文会简短一些,介绍下CastleCore作为代理库的一些缺点甚至是硬伤异步支持先上代码///<summary>///异常捕获、日志记录和耗时监控拦截器2024-1-1221:28:22///</summary>publicclassCatch......
  • 聊一聊 C# 的线程本地存储TLS到底是什么
    聊一聊C#的线程本地存储TLS到底是什么 一:背景1.讲故事有朋友在后台留言让我说一下C#的 ThreadStatic 线程本地存储是怎么玩的?这么说吧,C#的ThreadStatic是假的,因为C#完全是由CLR(C++)承载的,言外之意C#的线程本地存储,用的就是用C++运行时提供的 __declspec(thread) 或 ......
  • 聊一聊 C# 线程切换后上下文都去了哪里
    一:背景1.讲故事总会有一些朋友问一个问题,在Windows中线程做了上下文切换,请问被切的线程他的寄存器上下文都去了哪里?能不能给我挖出来?这个问题其实比较底层,如果对操作系统没有个体系层面的理解以及做过源码分析,其实很难说明白,这篇我们就从.NET高级调试的角度试着分析一下吧。二:寄......
  • 聊一聊 C# 线程切换后上下文都去了哪里
    一:背景1.讲故事总会有一些朋友是不是问一个问题,在Windows中线程做了上下文切换,请问被切的线程他的寄存器上下文都去了哪里?能不能给我挖出来?这个问题其实比较底层,如果对操作系统没有个体系层面的理解以及做过源码分析,其实很难说明白,这篇我们就从.NET高级调试的角度试着分析一......
  • 聊一聊 .NET高级调试 内核模式堆泄露
    一:背景1.讲故事前几天有位朋友找到我,说他的机器内存在不断的上涨,但在任务管理器中查不出是哪个进程吃的内存,特别奇怪,截图如下:在我的分析旅程中都是用户态模式的内存泄漏,像上图中的异常征兆已经明确告诉你了,不是用户态程序吃的内存,那就是内核态程序吃的,比如:某些驱动程序操......
  • 今天聊一聊高性能系统架构设计是什么样的
    Java全能学习+面试指南:https://javaxiaobear.cn今天聊一聊大家常听到的高性能系统架构。高性能系统架构,主要包括两部分内容,性能测试与性能优化。性能优化又可以细分为硬件优化、中间件优化、架构优化及代码优化,知识架构图如下。性能测试先看系统的性能测试。性能测试是性能优......
  • 聊一聊Rust的enum
    enum在实际编程中是非常常用的,enum的目的就是为了清晰定义出散落在系统各个角落的相同概念的有限固定值。一、enum介绍如果是简单定义固定值,我们可以使用常量const。比如publicconstintMAX_THREAD_COUNT=100;在C语言中,我们可以这样定义一个枚举方便各处使用,比如:enumDi......
  • 聊一聊 .NET高级调试 中必知的符号表
    一:背景1.讲故事在高级调试的旅行中,发现有不少人对符号表不是很清楚,其实简而言之符号表中记录着一些程序的生物特征,比如哪个地址是函数(签名信息),哪个地址是全局变量,静态变量,行号是多少,数据类型是什么等等,目的就是辅助我们可视化的调试,如果没有这些辅助我们看到的都是一些无意......