第十一章:C语言逆向(一):变量与常量、数组、指针、结构体的反汇编识别

各位同学,欢迎来到逆向工程的实战环节。从今天开始,我们要真正进入C语言的反汇编世界。说实话,很多初学者觉得汇编难,其实不是汇编本身难,而是你找不到C代码和汇编指令之间的对应关系。一旦你掌握了这个映射关系,逆向就像看翻译稿一样简单。

这一章,我们聚焦最基础的数据类型:变量、常量、数组、指针和结构体。这些东西在IDA或Ghidra里长什么样?怎么一眼认出它们?我带你走一遍。

11.1 变量与常量:栈上的“临时工”

先看局部变量。C语言里最常见的局部变量,说白了就是放在栈上的临时数据。你写个int a = 10;,编译器会怎么做?

// C源码
void func() {
    int a = 10;
    int b = 20;
    int c = a + b;
}

对应的x86-64汇编(未优化)大概是这样:

push   rbp
mov    rbp, rsp
mov    DWORD PTR [rbp-4], 10    ; a = 10
mov    DWORD PTR [rbp-8], 20    ; b = 20
mov    eax, DWORD PTR [rbp-4]
add    eax, DWORD PTR [rbp-8]
mov    DWORD PTR [rbp-12], eax  ; c = a + b
pop    rbp
ret

看到没?局部变量全部通过rbp-偏移量来访问。偏移量是负的,从-4、-8、-12依次往下排。我个人习惯在逆向时,先看函数开头有没有sub rsp, XX,这个XX就是栈帧大小,基本能猜出局部变量数量。

关键识别点:

  • 局部变量:[rbp-XX][rsp+XX] 形式
  • 常量:直接出现在指令中的立即数,如 mov eax, 10
  • 全局变量:通过固定地址访问,如 mov eax, [0x404000]

避坑指南:我曾经遇到一个加了-O2优化的函数,局部变量全被优化到寄存器里了,栈上根本看不到。这时候别慌,看寄存器操作就能还原逻辑。优化后的代码,变量可能不存在于内存中。

11.2 数组:连续内存的“排排坐”

数组在内存里就是连续的一块空间。你写int arr[5],编译器会分配20个字节(5*4)。访问arr[i]时,本质是基地址 + i * sizeof(元素)

// C源码
void array_demo() {
    int arr[4] = {1, 2, 3, 4};
    arr[2] = 100;
}

反汇编后:

push   rbp
mov    rbp, rsp
mov    DWORD PTR [rbp-16], 1    ; arr[0]
mov    DWORD PTR [rbp-12], 2    ; arr[1]
mov    DWORD PTR [rbp-8], 3     ; arr[2]
mov    DWORD PTR [rbp-4], 4     ; arr[3]
mov    DWORD PTR [rbp-8], 100   ; arr[2] = 100
pop    rbp
ret

你想想看,数组元素在栈上是连续递减的(从高地址到低地址)。arr[0]rbp-16arr[1]rbp-12,每个元素间隔4字节。如果看到这种等间距的栈访问模式,基本可以断定是数组。

对于循环遍历数组的情况,反汇编里会出现一个循环结构,配合基地址+索引*sizeof的寻址方式。比如:

movsxd rcx, dword ptr [rbp-20]   ; i
mov    eax, dword ptr [rbp+rcx*4-16]  ; arr[i]

这种[基址 + 索引 * 比例]的寻址模式,就是数组访问的铁证。

11.3 指针:内存的“导航员”

指针在逆向里是最容易让人迷糊的。其实指针就是个变量,只不过它存的是地址。你写int *p = &a;,反汇编里就是lea rax, [rbp-4],然后把rax存到栈上。

// C源码
void ptr_demo() {
    int a = 42;
    int *p = &a;
    *p = 100;
}

反汇编:

mov    DWORD PTR [rbp-4], 42    ; a = 42
lea    rax, [rbp-4]             ; rax = &a
mov    QWORD PTR [rbp-16], rax  ; p = &a (存到栈上)
mov    rax, QWORD PTR [rbp-16]  ; rax = p
mov    DWORD PTR [rax], 100     ; *p = 100

注意看:lea指令是取地址,不是取值。如果看到lea rax, [rbp-XX],然后把这个值存到另一个栈位置,那八成就是指针赋值。而mov DWORD PTR [rax], 100这种通过寄存器间接访问内存的,就是解引用。

重要提醒:指针的大小在64位系统上是8字节(QWORD),32位系统上是4字节(DWORD)。逆向时一定要先确认目标程序是32位还是64位,否则指针大小搞错,整个分析都会跑偏。我见过有人把64位的指针当成32位来读,结果地址被截断,分析了一整天发现是位数搞错了。

11.4 结构体:数据的“集装箱”

结构体在内存里就是把多个变量打包在一起。每个成员按声明顺序排列,中间可能有对齐填充。比如:

struct Point {
    int x;      // 偏移0
    int y;      // 偏移4
    char name;  // 偏移8
    // 填充3字节到12字节对齐
};

访问结构体成员时,反汇编里会看到基地址 + 固定偏移的模式。比如p->y就是[rbp-8](如果基址是rbp-12的话)。

// C源码
void struct_demo() {
    struct Point pt;
    pt.x = 10;
    pt.y = 20;
    pt.name = 'A';
}

反汇编:

mov    DWORD PTR [rbp-12], 10   ; pt.x (偏移0)
mov    DWORD PTR [rbp-8], 20    ; pt.y (偏移4)
mov    BYTE PTR [rbp-4], 65     ; pt.name (偏移8), 'A'=65

看到没?三个成员分别占用了rbp-12rbp-8rbp-4。偏移量分别是0、4、8,完全符合结构体定义。如果中间有奇怪的间隔,那就是对齐填充在作怪。

实战技巧:在IDA里,你可以用CreateStruct功能把一段连续内存定义成结构体。我一般先看代码里访问了哪些偏移,然后反推结构体布局。比如看到[rax+0][rax+4][rax+8],基本可以确定这是一个至少3个成员的结构体。

11.5 知识体系总览

下面这张图总结了C语言基本数据类型在反汇编中的识别要点,建议你保存下来随时对照。

C语言反汇编识别:数据类型篇 变量与常量 数组 指针 结构体 特征 • [rbp-XX] 栈访问 • 立即数直接赋值 • 全局固定地址 特征 • 等间距栈偏移 • [基址+索引*比例] • 循环+基址模式 特征 • lea取地址 • 间接内存访问 • QWORD存储(64位) 特征 • 固定偏移访问 • 连续内存布局 • 对齐填充空隙 核心方法论 1. 先看函数栈帧大小(sub rsp, XX) 2. 识别所有 [rbp-XX] 和 [rsp+XX] 访问 3. 根据偏移间距和寻址模式推断数据类型 4. 结合上下文(循环、赋值、函数调用)验证推断

11.6 综合实战:一眼看穿结构体指针数组

最后,我们看一个稍微复杂点的例子,把前面几个知识点串起来。假设有这样一个C结构:

struct Student {
    int id;        // 0
    char grade;    // 4
    // 填充3字节
    long score;    // 8
};

void process(struct Student *s) {
    s->id = 1001;
    s->grade = 'A';
    s->score = 95;
}

反汇编后:

process:
    push   rbp
    mov    rbp, rsp
    mov    QWORD PTR [rbp-8], rdi   ; 参数s存到栈上
    mov    rax, QWORD PTR [rbp-8]   ; rax = s
    mov    DWORD PTR [rax], 1001    ; s->id = 1001
    mov    rax, QWORD PTR [rbp-8]   ; rax = s
    mov    BYTE PTR [rax+4], 65     ; s->grade = 'A'
    mov    rax, QWORD PTR [rbp-8]   ; rax = s
    mov    QWORD PTR [rax+8], 95    ; s->score = 95
    pop    rbp
    ret

你看,通过偏移量0、4、8,我立刻能还原出结构体有三个成员:4字节的int、1字节的char、8字节的long。中间从偏移5到7是3字节填充。这就是逆向分析结构体的标准流程。

我的个人习惯:在IDA里遇到这种模式,我会直接按Alt+Q打开结构体编辑器,新建一个结构体,把成员按偏移填进去。然后选中代码里的[rax+0],按T键转换成结构体访问。这样代码可读性瞬间提升10倍。

嗯,这一章的内容就到这里。变量、常量、数组、指针、结构体,这些是C语言逆向的基石。你把这些识别模式练熟了,后面遇到函数调用、虚函数、异常处理等高级特性时,才能游刃有余。记住:逆向没有捷径,多读反汇编,多总结模式,你的眼力会越来越毒。


公众号:蓝海资料掘金营,微信deep3321