首页
社区
课程
招聘
[原创]从GDB中观察x86-64数组、指针、结构体在汇编中的表示
发表于: 2天前 573

[原创]从GDB中观察x86-64数组、指针、结构体在汇编中的表示

2天前
573

测试代码

#include <stdio.h>
#include <string.h>

struct Student {
    int id;
    int score;
    char name[16];
};

void process_array() {
    int arr[5] = {10, 20, 30, 40, 50};
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += arr[i];
    }
    printf("sum = %d\n", sum);
}

void process_pointer() {
    int x = 42;
    int *p = &x;
    *p = 100;
    printf("x = %d\n", x);
}

void process_struct() {
    struct Student s;
    s.id = 1;
    s.score = 95;
    strcpy(s.name, "Alice");
    printf("id=%d score=%d name=%s\n", s.id, s.score, s.name);
}

int main() {
    process_array();
    process_pointer();
    process_struct();
    printf("%d\n",sizeof (struct Student));
    return 0;
}

运行环境:

    Ubuntu 20.04.6 LTS

编译指令:

    gcc -g -O0 -o main main.c

gcc版本

    gcc version 9.4.0

调试工具:

    GDB


开始执行

进入main 函数:

=> 0x5555555552d0 <main>: endbr64 

      0x5555555552d4 <main+4>: push   %rbp

      0x5555555552d5 <main+5>: mov    %rsp,%rbp

      0x5555555552d8 <main+8>: mov    $0x0,%eax

      0x5555555552dd <main+13>: callq  0x555555555169 <process_array>


   传入了一个0到process_array函数内部,但是他没用容rdi去传参,那process_array函数参数可能是一个不定数量的参数

进入process_array函数内部:

这段代码中构建了栈帧,存入了金丝雀的值

   0x55555555516d <process_array+4>: push   %rbp

   0x55555555516e <process_array+5>: mov    %rsp,%rbp

   0x555555555171 <process_array+8>: sub    $0x30,%rsp

   0x555555555175 <process_array+12>: mov    %fs:0x28,%rax

   0x55555555517e <process_array+21>: mov    %rax,-0x8(%rbp)


然后清零了eax: 可是eax不是传入的不定长参数浮点数的数量么?不确定 再看看

0x555555555182 <process_array+25>: xor    %eax,%eax


继续执行:这段代码在一个连续的区域初始化了一些值:可能是一个数组,

=> 0x555555555184 <process_array+27>: movl   $0xa,-0x20(%rbp)

      0x55555555518b <process_array+34>: movl   $0x14,-0x1c(%rbp)

      0x555555555192 <process_array+41>: movl   $0x1e,-0x18(%rbp)

      0x555555555199 <process_array+48>: movl   $0x28,-0x14(%rbp)

      0x5555555551a0 <process_array+55>: movl   $0x32,-0x10(%rbp)


      查看内存,验证内容

      (gdb) x/10x $rbp-0x20

      0x7fffffffdcf0: 0x0000000a 0x00000014 0x0000001e 0x00000028

      0x7fffffffdd00: 0x00000032

继续执行:看起来是设置了两个变量值为0

=> 0x5555555551a7 <process_array+62>: movl   $0x0,-0x28(%rbp)

      0x5555555551ae <process_array+69>: movl   $0x0,-0x24(%rbp)


继续执行:

=> 0x5555555551b5 <process_array+76>: jmp    0x5555555551c7 <process_array+94>

      0x5555555551b7 <process_array+78>: mov    -0x24(%rbp),%eax

      0x5555555551ba <process_array+81>: cltq   

      0x5555555551bc <process_array+83>: mov    -0x20(%rbp,%rax,4),%eax

      0x5555555551c0 <process_array+87>: add    %eax,-0x28(%rbp)

      0x5555555551c3 <process_array+90>: addl   $0x1,-0x24(%rbp)

      0x5555555551c7 <process_array+94>: cmpl   $0x4,-0x24(%rbp)

      0x5555555551cb <process_array+98>: jle    0x5555555551b7 <process_array+78>

      0x5555555551cd <process_array+100>: mov    -0x28(%rbp),%eax


发现一个循环:跳转去0x5555555551c7位置执行代码:

使用了 rbp-0x24中的值和0x4进行比较,判断 rbp-0x24是否小于等于4如果小于进入循环体

先将rbp-4放在了eax中,

ovl   $0x0,-0x24(%rbp)

然后可能进行了强制类型转换从4字节升到了8字节,观察后面代码发现,是为了和rbp进行计算,那也就是eax是数组的下标

rbp-4就是循环中的累加参数。

mov    -0x20(%rbp,%rax,4),%eax

add    %eax,-0x28(%rbp)

从 数组的末尾取出一个值? 然后累加到 rbp-28这个参数内  ,此处发现,编译器并没有按照数组的顺序来进行累加

而是直接从数据末尾去累加的,再次印证了在-O0下可能不会按照顺序去执行代码

尝试还原代码:

int arr[5] = {0x32, 0x28, 0x1e, 0x14, 0xa}; 

int x=0;

for (int i = 0; i <= 4; i++){

x += arr[i];

}

继续执行:

=> 0x5555555551cd <process_array+100>: mov    -0x28(%rbp),%eax

      0x5555555551d0 <process_array+103>: mov    %eax,%esi

      0x5555555551d2 <process_array+105>:

      lea    0xe2b(%rip),%rdi        # 0x555555556004

      0x5555555551d9 <process_array+112>: mov    $0x0,%eax

      0x5555555551de <process_array+117>: callq  0x555555555070 <printf@plt>


调用printf函数 输出0x96 = 150


继续执行:判断金丝雀的值,后返回main函数

=> 0x5555555551e3 <process_array+122>: nop

      0x5555555551e4 <process_array+123>: mov    -0x8(%rbp),%rax

      0x5555555551e8 <process_array+127>: xor    %fs:0x28,%rax

      0x5555555551f1 <process_array+136>: je     0x5555555551f8 <process_array+143>

      0x5555555551f3 <process_array+138>: callq  0x555555555060 <__stack_chk_fail@plt>

      0x5555555551f8 <process_array+143>: leaveq 

      0x5555555551f9 <process_array+144>: retq   


继续执行:

=> 0x5555555552e2 <main+18>: mov    $0x0,%eax

      0x5555555552e7 <main+23>: callq  0x5555555551fa <process_pointer>


发现又一次清空了eax的值,并不清楚为什么要清零先进入process_pointer函数

=> 0x5555555551fa <process_pointer>: endbr64 

      0x5555555551fe <process_pointer+4>: push   %rbp

      0x5555555551ff <process_pointer+5>: mov    %rsp,%rbp

      0x555555555202 <process_pointer+8>: sub    $0x20,%rsp

      0x555555555206 <process_pointer+12>: mov    %fs:0x28,%rax

      0x55555555520f <process_pointer+21>: mov    %rax,-0x8(%rbp)

      0x555555555213 <process_pointer+25>: xor    %eax,%eax

     构建栈帧, 保存金丝雀

 继续执行:

=> 0x555555555215 <process_pointer+27>: movl   $0x2a,-0x14(%rbp)

      0x55555555521c <process_pointer+34>: lea    -0x14(%rbp),%rax

      0x555555555220 <process_pointer+38>: mov    %rax,-0x10(%rbp)


   将0x2a的值放在了 rbp-0x14位置

   (gdb) x/10x $rbp-0x14

0x7fffffffdcfc: 0x0000002a 0xffffdcfc 0x00007fff 0xa69bd200


   那可能是 int a = 0x2a;


   然后取了rbp-0x14的地址放在了rax中,然后就放在了rbp-0x10中

   那可能就是  

   int *b = &a;


继续执行:

=> 0x555555555224 <process_pointer+42>: mov    -0x10(%rbp),%rax

      0x555555555228 <process_pointer+46>: movl   $0x64,(%rax)

      0x55555555522e <process_pointer+52>: mov    -0x14(%rbp),%eax

      0x555555555231 <process_pointer+55>: mov    %eax,%esi

      0x555555555233 <process_pointer+57>:        lea    0xdd4(%rip),%rdi        # 0x55555555600e

      0x55555555523a <process_pointer+64>: mov    $0x0,%eax

      0x55555555523f <process_pointer+69>: callq  0x555555555070 <printf@plt>


   movl   $0x64,(%rax),修改了指针b指向的值

   那就是 *b = 0x64; 此时 a = 0x64即 rbp-0x14中的值为0x64

(gdb) x/10x $rbp-0x14

0x7fffffffdcfc: 0x00000064 0xffffdcfc 0x00007fff 0xa69bd200


   然后调用printf函数,输出了a的值


继续执行:返回main函数

   0x555555555244 <process_pointer+74>: nop

   0x555555555245 <process_pointer+75>: mov    -0x8(%rbp),%rax

   0x555555555249 <process_pointer+79>: xor    %fs:0x28,%rax

   0x555555555252 <process_pointer+88>:

    je     0x555555555259 <process_pointer+95>

   0x555555555254 <process_pointer+90>:

    callq  0x555555555060 <__stack_chk_fail@plt>

   0x555555555259 <process_pointer+95>: leaveq 

   0x55555555525a <process_pointer+96>: retq 


继续执行:

=> 0x5555555552ec <main+28>: mov    $0x0,%eax

      0x5555555552f1 <main+33>: callq  0x55555555525b <process_struct>


   发现调用前仍然让eax的值为0,猜测可能是因为上面的函数是void类型,所以提前写好了返回值


进入process_struct函数:

=> 0x55555555525b <process_struct>: endbr64 

      0x55555555525f <process_struct+4>: push   %rbp

      0x555555555260 <process_struct+5>: mov    %rsp,%rbp

      0x555555555263 <process_struct+8>: sub    $0x20,%rsp

      0x555555555267 <process_struct+12>: mov    %fs:0x28,%rax

      0x555555555270 <process_struct+21>: mov    %rax,-0x8(%rbp)

      0x555555555274 <process_struct+25>: xor    %eax,%eax


   老样子,构建栈帧,拿金丝雀的值存起来


继续执行:

=> 0x555555555276 <process_struct+27>: movl   $0x1,-0x20(%rbp)

      0x55555555527d <process_struct+34>: movl   $0x5f,-0x1c(%rbp)


      0x555555555284 <process_struct+41>: lea    -0x20(%rbp),%rax

      0x555555555288 <process_struct+45>: add    $0x8,%rax

      0x55555555528c <process_struct+49>: movl   $0x63696c41,(%rax)

      0x555555555292 <process_struct+55>: movw   $0x65,0x4(%rax)


      0x555555555298 <process_struct+61>: mov    -0x1c(%rbp),%edx

      0x55555555529b <process_struct+64>: mov    -0x20(%rbp),%eax

      0x55555555529e <process_struct+67>: lea    -0x20(%rbp),%rcx


      0x5555555552a2 <process_struct+71>: add    $0x8,%rcx

      0x5555555552a6 <process_struct+75>: mov    %eax,%esi

      0x5555555552a8 <process_struct+77>: lea    0xd67(%rip),%rdi        # 0x555555556016

      0x5555555552af <process_struct+84>: mov    $0x0,%eax



   先是在一块连续的区域存储了两个4字节的数据, 0x1和0x5f  即arr[2] = {0x5f, 0x1};

   然后取rbp-0x20这个地址放在了rax中, 让这个地址+8  此时rax就等于 rbp-0x20+0x8这个地址为rbp-0x18


   然后将0x63696c41放入了rax的地址rbp-0x18中,gdb中观察到这个是一个字符串Alic

   后将rax+0x4 = rbp-0x18+0x4 = rbp-0x14 位置写入了2字节0x65  -> movw   $0x0065,0x4(%rax) 最后一个字符的值,以及/0

   有意思的是,他并没有一次写入字符串的值,而是分两次去写入了字符串的值


   查阅后发现,在strcpy函数或者类似的字符串操作在被编译器优化后的表现形式,通过指针来写入内存


   那可能就是 char *a = "ALice";


   此时的栈帧结构可能是

   rbp-> 栈帧

   rbp-0x8 = 金丝雀


   rbp-0x14 = 0x0065 = e\0

   rbp-0x18=0x63696c41 = Alic

   rbp-0x1c=0x5f  4字节

   rbp-0x20=1     4字节


   然后调用printf函数,传入了

   rdi 参数1 格式化字符串, esi 参数2 ebp-20的值1, edx 参数3 ebp-1c的值 0x5f; ecx参数4 rbp-0x18的地址

   因为参数是连续的,并且数据大小不一致,判断此前的赋值操作都是对结构体进行赋值

   结构体的结构就可能是

   struct {

    char b[]; 具体字符上限未知,如果是指针的话,代码应该会有开辟堆空间的动作,没看到就认为是一个字符数组

    int c;

    int d;

   }

   s.a = 0x65;

   strcpy(s.b, "Alice");

   d = 1;


   printf("字符串", 1, 0x5f, "Alice");


关于调用函数前将eax赋值为0的试验

尝试试验:

修改函数

float process_array(float x) {
    int arr[5] = {10, 20, 30, 40, 50};
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += arr[i];
    }
    printf("sum = %d\n", sum);
    return x;
}


编译后反汇编

    12d9:       f3 0f 1e fa             endbr64 

    12dd:       55                      push   %rbp

    12de:       48 89 e5                mov    %rsp,%rbp

    12e1:       f3 0f 10 05 4b 0d 00    movss  0xd4b(%rip),%xmm0        # 2034 <_IO_stdin_used+0x34>

    12e8:       00 

    12e9:       e8 7b fe ff ff          callq  1169 <process_array>

    12ee:       b8 00 00 00 00          mov    $0x0,%eax

没有传递al

再次修改

int process_array(int x) {
    int arr[5] = {10, 20, 30, 40, 50};
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += arr[i];
    }
    printf("sum = %d\n", sum);
    return x;
}


编译后反汇编

00000000000012d5 <main>:

    12d5:       f3 0f 1e fa             endbr64 

    12d9:       55                      push   %rbp

    12da:       48 89 e5                mov    %rsp,%rbp

    12dd:       bf 01 00 00 00          mov    $0x1,%edi

    12e2:       e8 82 fe ff ff          callq  1169 <process_array>


因为rax是函数的返回值,有没有可能是因为函数的返回值为空所以提前设置了0呢,再次试验

int process_array() {
    int arr[5] = {10, 20, 30, 40, 50};
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += arr[i];
    }
    printf("sum = %d\n", sum);
    return 1;
}


编译后反汇编:

00000000000012d4 <main>:

    12d4:       f3 0f 1e fa             endbr64 

    12d8:       55                      push   %rbp

    12d9:       48 89 e5                mov    %rsp,%rbp

    12dc:       b8 00 00 00 00          mov    $0x0,%eax

    12e1:       e8 83 fe ff ff          callq  1169 <process_array>

发现是没有关系的。


现象为 函数参数为空时,会在调用函数之前加上一句 mov eax,0 但是不为空时,则不会。



[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回