菜鸟笔记之PWN入门(1.1.1)汇编语言基础与堆栈入门

啥是汇编语言?有啥用?

深入了解计算机底层,我们会发现,计算机实际上只能执行一些非常基础的操作,但其速度却非常快。计算机的CPU只能执行机器码,即由一系列0和1组成的指令。不同的0和1组合会触发计算机中的不同电路,从而进行各种操作。由于这些0和1的组合很长,阅读起来不方便,因此通常以16进制形式显示。然而,即便如此,16进制的表示仍然难以理解。为了简化这一过程,汇编语言应运而生,它用更易读的符号来表示这些机器指令,从而使程序员能够更方便地编写和理解代码。

啥是寄存器?

计算机中的所有指令都是由CPU执行的。在计算机结构中,CPU和内存是分开的,而寄存器则位于CPU内部。CPU可以直接访问寄存器中的数据来执行各种指令操作。在程序执行过程中,数据通常从内存 –> 缓存(cache)–> 寄存器,只有到了寄存器中CPU才能对其进行处理。处理完成后,程序决定是否将寄存器中的数据写回内存,以更新内存中的内容。除了存储数据,寄存器还可以用于存储指令、地址、状态信息和其他控制数据。因此,寄存器在计算机中发挥着至关重要的作用,帮助提高处理速度和效率。
下图表介绍一些常用的寄存器

什么是栈?什么是堆?

先贴一张图用来理清楚堆和栈在程序中的位置

栈的简单介绍

在计算机程序运行时,系统会将可执行文件的text段(代码段)和data段(数据段)加载到内存中。在C语言等编程语言中,函数调用过程中需要传递参数创建局部变量。如果这些数据都存储在data段bss段中,会导致内存空间的浪费,并使得管理变得复杂。
为了高效管理函数的参数和局部变量,并在函数调用结束后释放相关资源,我们使用了是一种后进先出(LIFO)的数据结构,它能够在函数调用时创建栈帧来存储函数的局部变量、参数、返回地址等信息。当函数调用结束时,栈帧会被自动销毁,从而释放内存,避免内存浪费。栈不仅节省内存,还确保了函数调用和返回过程的有序进行。

ESP(栈指针)和 EBP(基址指针)之间的空间被称为栈帧栈帧是一个函数调用的栈上空间,主要用于存储函数的局部变量参数返回地址以及保存的寄存器状态等信息。在每次函数调用时,都会为该函数分配一个新的栈帧,并在函数执行完成后释放这个栈帧。栈帧的结构通常如下:

  • **栈顶 (由 ESP 指针指向)**:函数调用时,栈顶包含函数的局部变量和临时数据。
  • **栈底 (由 EBP 指针指向)**:栈底用于存储函数调用的参数和返回地址等。

栈帧的设计使得函数调用和返回变得高效,并且支持递归调用等复杂的函数调用模式。

堆的简单介绍

堆是用于存放进程运行中动态分配的内存段,其大小可以在运行时根据需求调整。调用malloccallocrealloc等函数时,会从堆上分配内存。释放内存时使用free函数,以便回收堆内存。堆内存的管理由程序员负责,不会自动释放,需要手动管理内存的分配和释放。

浅浅总结一下:

用于存放各个函数所需要使用的参数,比如:

1
2
3
4
5
6
7
#include <stdio.h>
int main() {
char str[30]; // str是一个局部变量,存储在栈上
scanf("%29s", str); // 读取字符串到str中
printf("%s", str); // 输出str的内容
return 0;
}

在这个例子中,str是一个局部数组,分配在栈上。

则是用于存放用户的数据,并且由用户决定什么时候释放它。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 100

void create_heap(char **heap_memory) {
*heap_memory = (char *)malloc(BUFFER_SIZE * sizeof(char));
if (*heap_memory == NULL) {
printf("Memory allocation failed\n");
exit(1);
}
printf("Heap memory created.\n");
}

void edit_heap(char *heap_memory) {
if (heap_memory == NULL) {
printf("Heap memory not created yet.\n");
return;
}
printf("Enter a string to store in heap memory: ");
fgets(heap_memory, BUFFER_SIZE, stdin);

heap_memory[strcspn(heap_memory, "\n")] = '\0';
printf("Heap memory updated.\n");
}

void delete_heap(char *heap_memory) {
if (heap_memory == NULL) {
printf("Heap memory not created yet.\n");
return;
}
free(heap_memory);
heap_memory = NULL;
printf("Heap memory deleted.\n");
}

int main() {
char *heap_memory = NULL;
int choice;

while (1) {
printf("\nMenu:\n");
printf("1. Create heap memory\n");
printf("2. Edit heap memory\n");
printf("3. Delete heap memory\n");
printf("4. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
getchar();

switch (choice) {
case 1:
create_heap(&heap_memory);
break;
case 2:
edit_heap(heap_memory);
break;
case 3:
delete_heap(heap_memory);
break;
case 4:
if (heap_memory != NULL) {
free(heap_memory);
}
printf("Exiting program.\n");
return 0;
default:
printf("Invalid choice. Please try again.\n");
break;
}
}
}

(这里例子可能举的有点复杂…但是之后遇到的的题目大多也是类似这样的,所以堆的难度会比大很多,知识量也多很多)
总之上面的代码就是一般堆的用法,不需要理解,只是作为展示,大致内容如下:
输入1,用户可以申请一个堆的空间
输入2,用户可以编辑所申请到的空间
输入3,用户可以删除申请到的空间

常用汇编指令与C语言对比

  1. MOV

    • 功能:将数据从一个位置复制到另一个位置。
    • 汇编MOV AX, 1234h
    • C语言AX = 0x1234;
    • 说明MOV 是把数据从源位置复制到目标位置,类似于C语言中的赋值操作。MOV 不改变源数据,只是复制到目标位置。
  2. ADD / SUB

    • 功能:进行加法和减法运算。
    • 汇编ADD AX, BXAX = AX + BX);SUB CX, 5CX = CX - 5
    • C语言AX += BX;AX = AX + BX);CX -= 5;CX = CX - 5
    • 说明ADD 和 SUB 用于对寄存器或内存中的值进行加减操作,更新结果并设置标志寄存器的状态。
  3. INC / DEC

    • 功能:递增或递减操作数。
    • 汇编INC AXAX = AX + 1);DEC BXBX = BX - 1
    • C语言AX++;AX = AX + 1);BX--;BX = BX - 1
    • 说明INC 和 DEC 只能对寄存器或内存位置操作,不能对立即数操作。
  4. CMP

    • 功能:比较两个值,并设置标志寄存器状态。
    • 汇编CMP AX, BXif (AX == BX)
    • C语言if (AX == BX)AX 和 BX 比较)
    • 说明CMP 实际上是做了 AX - BX 的操作然后判断是否等于0,更新状态标志。
  5. JMP

    • 功能:无条件跳转到指定的地址。
    • 汇编JMP start
    • C语言goto start;
    • 说明JMP 改变程序的执行流,跳转到标记为 start 的位置。
  6. JE / JNE / JZ / JNZ

    • 功能:根据条件跳转到指定地址。
    • 汇编JE equalif (zero flag) goto equal;);JNE not_equalif (not equal) goto not_equal;
    • C语言if (flag == 0) goto equal;JE 跳转条件为零标志);if (flag != 0) goto not_equal;JNE 跳转条件为非零)
    • 说明:这些指令依据先前比较操作的结果决定是否跳转。
  7. CALL / RET

    • 功能:调用和返回函数。
    • 汇编CALL myFunction(调用函数);RET(返回函数)
    • C语言myFunction();(调用函数);return;(从函数返回)
    • 说明CALL 会保存当前的返回地址,并跳转到目标函数;RET 从栈中恢复返回地址并跳转回。
  8. PUSH / POP

    • 功能:压入或弹出栈数据。
    • 汇编PUSH AX(压入栈);POP BX(从栈中弹出)
    • C语言stack.push(AX);(假设有栈操作的C语言函数);BX = stack.pop();(假设有栈操作的C语言函数)
    • 说明PUSH 和 POP 操作栈,PUSH 将数据压入栈顶,POP 从栈顶弹出数据。
  9. NOP

    • 功能:空操作,不产生任何效果。
    • 汇编NOP
    • C语言:没有直接的C语言对应,可以理解为一个空的操作(或者可以理解成C语言的注释符号)。
    • 说明NOP 通常用于占位或调试,不会改变程序状态。
  10. LEA

    • 功能:加载有效地址,将计算得到的地址赋给寄存器。
    • 汇编LEA AX, [BX+SI]
    • C语言AX = &array[BX + SI];(假设 array 是一个数组)
    • 说明LEA 计算一个地址并将其存入寄存器,类似于获取指针地址而不实际访问内存。
  11. AND / OR / XOR / NOT

    • 功能:按位操作。
    • 汇编AND AX, BX(按位与);OR AX, BX(按位或);XOR AX, BX(按位异或);NOT AX(按位取反)
    • C语言AX = AX & BX;(按位与);AX = AX | BX;(按位或);AX = AX ^ BX;(按位异或);AX = ~AX;(按位取反)
    • 说明:这些指令用于位级操作,AND 和 OR 用于逻辑运算,XOR 用于位翻转,NOT 对每一位取反。

这里重点介绍一下PUSH POP 这两个指令
PUSHPOP是对进行操作的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我 是 栈 ↓
----
| 5 | <- 栈底ebp:注意,我是栈底,我在高地址
----
| 4 |
----
| 3 |
----
| 2 |
----
| 1 | <- 栈顶esp:注意,我是栈顶,我在低地址
----
假设这是栈最开始的样子
假设:
eax = 6
ebx = 7

我浅浅画了一个非常抽象的,但是这并不影响我们理解它。
当我们执行 POP ebx 这个指令后,那么就会让栈变成这个样子↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我 是 栈 ↓
----
| 5 | <- 栈底ebp:我不动,与我无瓜
----
| 4 |
----
| 3 |
----
| 2 | <- 栈顶esp:执行完POP后,我会向高地址移动,现在我指向2
----
| 1 | <-- 1没有消失,只是不在esp和ebp之间的栈帧了,这导致我们无法直接访问
----

eax = 6
ebx = 1 <-- 注意注意,ebx变成1

然后来看看PUSH eax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我 是 栈 ↓
----
| 5 | <- 栈底ebp:我还是不动,依旧与我无瓜
----
| 4 |
----
| 3 |
----
| 2 |
----
| 6 | <- 栈顶esp:执行完PUSH后,我会向低地址地址移动,并且将eax的值,也就是6
---- 放入栈当中,替换掉了1

eax = 6
ebx = 1

为什么函数调用需要用栈

首先为什么后进先出(LIFO),这样的设计是因为程序的执行符合一种将复杂的大问题拆解成一个个小问题来解决的思想。
大问题的解决需要小问题的答案,而小问题得到答案之后就可以抛弃了。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h> // 如果需要使用标准输入输出函数

void func1() {
printf("hello");
}
void func2() {
func1();
}
void func3() {
func2();
}
int main() {
func3();
return 0;
}

那么这个程序的执行流程就是 main --> func1 --> func2 --> func3 --> printf
那么栈的空间就是:

1
2
3
4
5
6
7
8
9
10
11
 ---------------
| main 的栈空间 | <- 栈底 ebp(高地址)
---------------
| func1 的栈空间 |
---------------
| func2 的栈空间 |
---------------
| func3 的栈空间 |
---------------
| printf 的栈空间| <- 栈顶 esp(低地址)
---------------

我们得出规律,大问题函数程序总是最后消失
也就是调用者的生命周期总是长于被调用者
这恰好符合了FILO的特点。所以一定不是被随意设计的,而是一种完美符合计算机程序设计思想的一种数据结构

参考文献:
什么是堆?什么是栈?他们之间有什么区别和联系? - tolin - 博客园 (cnblogs.com)
PWN入门(1-1-2)-bss段、data段、text段、堆(heap)和栈(stack) (yuque.com)
在x86架构中的寄存器 - 知乎 (zhihu.com)
什么是栈,为什么函数式编程语言都离不开栈?_没有栈,都是跟栈有什么关系-CSDN博客
PWN入门(1-1-1)-C函数调用过程原理及函数栈帧分析(Intel) (yuque.com)
linux - C函数调用过程原理及函数栈帧分析 - 编程之道 - SegmentFault 思否


菜鸟笔记之PWN入门(1.1.1)汇编语言基础与堆栈入门
http://example.com/2024/10/05/菜鸟笔记之PWN入门(1.1.1)汇编语言基础与堆栈入门/
作者
XiDP
发布于
2024年10月5日
许可协议