寄存器

通用寄存器:

  • eax(累加器):常用与算数、逻辑运算和函数返回值等操作
  • ebx(基址寄存器):可用于存储内存基地址,在访问内存数据时发挥作用
  • ecx (循环计数器):在循环指令中,自动对循环次数进行计数
  • edx(数据寄存器):用于存储数据,常配合 eax 进行乘除等运算
  • esi、edi(索引寄存器):主要用于字符串操作和内存数据的索引寻址
  • esp(堆栈指针):指向栈顶,用于管理程序运行时的堆栈
  • ebp(基址指针):指向当前栈帧的底部,辅助访问函数栈帧中的局部变量和参数
  • eip(指令指针):存放下一条要执行的指令的地址

段寄存器:

  • cd(代码段寄存器):指向当前正在执行的代码所在的段
  • ds(数据段寄存器):指向程序使用的数据所在的段

上图中右侧图表展示了通用寄存器的结构,以 eax 为例,它是 32 位寄存器,可拆分为 16 位的 ax,ax 又能进一步分为 8 位的 ah 和 al,其他通用寄存器也有类似的结构关系

指令集

mov

  • 指令功能:“mov” 是数据传送指令,它的作用是将源操作数(source)的值传送到目标操作数(dest)中
  • 语法结构:格式为 “mov dest, source”,其中目标操作数可以是寄存器或内存地址,源操作数可以是寄存器、内存地址或立即数(常数)
  • 例子讲解:
    1. “mov eax, [ebx]”:将寄存器 ebx 所指向的内存地址中的值传送到寄存器 eax 中
    2. “mov eax, [ebx]”:将寄存器 ebx 所指向的内存地址中的值传送到寄存器 eax 中
    3. “mov [var], ebx”:把寄存器 ebx 中的值传送到名为 “var” 的内存位置中

push pop

  • 指令功能:
    1. “push” 是压栈指令,用于将数据存入堆栈。在操作时,堆栈指针(esp)会先减小相应的字节数,然后将数据存储到新的栈顶位置
    2. “pop” 是出栈指令,用于从堆栈中取出数据。执行时,先从当前栈顶位置读取数据,然后堆栈指针(esp)会增加相应的字节数
  • 例子讲解:
    1. “push eax”:将寄存器 eax 中的值压入堆栈
    2. “push 0”:把立即数 0 压入堆栈
    3. “pop eax”:从堆栈中弹出一个值,并将其存入寄存器 eax 中
    4. “pop [ebx]” :从堆栈中弹出一个值,并将其存入 ebx 寄存器所指向的内存地址中

Arithmetic

  • 指令概述:“add, sub, mul, div” 分别是加法(Addition)、减法(Subtraction)、乘法(Multiplication)和除法(Division )的算术运算指令 ;“inc” 和 “dec” 分别是递增(Increment)和递减(Decrement)指令,用于将操作数的值加 1 或减 1
  • 语法说明:“add” 指令需要两个操作数,格式为 “add 目标操作数(dest), 源操作数(source)”,表示将源操作数的值加到目标操作数上;“inc” 指令的操作数可以是寄存器()或内存位置(),使对应的值增加 1
  • 例子讲解:“add eax, 10” 表示将数值 10 加到寄存器 eax 中的值上;“inc eax” 表示将寄存器 eax 中的值增加 1

jump

  • 无条件跳转指令:“jmp” 是无条件跳转指令,一旦执行该指令,程序会直接跳转到指定的目标地址(标签处)继续执行
  • 条件跳转指令:
    1. “je
    2. “jne
    3. “jz
    4. “jg
    5. “jge
    6. “jl
    7. “jle

cmp

  • 指令功能:“cmp” 是比较指令,通过对两个操作数执行减法操作,根据结果设置标志寄存器(如零标志位 ZF、进位标志位 CF 等),但不保存运算结果。主要用于后续的条件判断跳转
  • 例子讲解:
    1. “cmp DWORD PTR [eax], 10”:将 eax 寄存器所指向的内存地址中的双字(DWORD,4 字节)数据与立即数 10 进行比较,根据比较结果设置标志位。之后 “je loop” 表示如果比较结果相等(即零标志位 ZF 被置位),则跳转到 “loop” 标签处执行。
    2. “cmp eax, ebx”:比较寄存器 eax 和 ebx 中的值,根据比较结果设置标志位。随后 “jle done” 表示如果 eax 中的值小于或等于 ebx 中的值,就跳转到 “done” 标签处。
    3. “jmp DWORD PTR [eax]”:这是一个跳转指令,将程序执行流跳转到 eax 寄存器所指向的内存地址处的双字数据指定的地址继续执行,与 “cmp” 指令配合,用于更复杂的程序控制流。

系统调用

  • 系统调用的作用:系统调用(Syscalls)是用户程序和 Linux 内核之间的接口。通过系统调用,用户程序可以请求内核提供的服务,如文件操作、进程管理、网络通信等
  • 寄存器的使用:
    • 在进行系统调用前,需要将相关值存入寄存器。其中 eax 寄存器用于表示系统调用号,不同的系统调用号对应不同的内核服务。
    • ebx、ecx 等其他寄存器用于传递系统调用所需的参数。
  • 执行过程:准备好寄存器中的值后,通过执行 “int 0x80” 指令触发系统调用,使程序从用户态切换到内核态,内核根据 eax 中的系统调用号执行相应的操作
  • 返回值:内核完成系统调用后,会将返回值存放在 eax 寄存器中,供用户程序获取并处理

例子

例如以下程序:

#include<stdio.h>
int sum(int i,int j){
    int sum;
    sum=i+j;
    return sum;
}
int main(void){
    int i;
    int j;
    int k;
    scanf("%d%d",&i,&j);
    k=sum(i,j);
    printf("Sum:%d\n",k);
    return 0;
}

使用以下命令:

gcc -m32 -o hello hello.c


下面是我自己编译出来的,和图片上的一样

pwndbg> disassemble main
Dump of assembler code for function main:
   0x000011bd <+0>:     lea    ecx,[esp+0x4]
   0x000011c1 <+4>:     and    esp,0xfffffff0
   0x000011c4 <+7>:     push   DWORD PTR [ecx-0x4]
   0x000011c7 <+10>:    push   ebp
   0x000011c8 <+11>:    mov    ebp,esp
   0x000011ca <+13>:    push   ebx
   0x000011cb <+14>:    push   ecx
   0x000011cc <+15>:    sub    esp,0x10
   0x000011cf <+18>:    call   0x10a0 <__x86.get_pc_thunk.bx>
   0x000011d4 <+23>:    add    ebx,0x2e20
   0x000011da <+29>:    sub    esp,0x4
   0x000011dd <+32>:    lea    eax,[ebp-0x14]
   0x000011e0 <+35>:    push   eax
   0x000011e1 <+36>:    lea    eax,[ebp-0x10]
   0x000011e4 <+39>:    push   eax
   0x000011e5 <+40>:    lea    eax,[ebx-0x1fec]
   0x000011eb <+46>:    push   eax
   0x000011ec <+47>:    call   0x1050 <__isoc99_scanf@plt>
   0x000011f1 <+52>:    add    esp,0x10
   0x000011f4 <+55>:    mov    edx,DWORD PTR [ebp-0x14]
   0x000011f7 <+58>:    mov    eax,DWORD PTR [ebp-0x10]
   0x000011fa <+61>:    sub    esp,0x8
   0x000011fd <+64>:    push   edx
   0x000011fe <+65>:    push   eax
   0x000011ff <+66>:    call   0x119d <sum>
   0x00001204 <+71>:    add    esp,0x10
   0x00001207 <+74>:    mov    DWORD PTR [ebp-0xc],eax
   0x0000120a <+77>:    sub    esp,0x8
   0x0000120d <+80>:    push   DWORD PTR [ebp-0xc]
   0x00001210 <+83>:    lea    eax,[ebx-0x1fe7]
   0x00001216 <+89>:    push   eax
   0x00001217 <+90>:    call   0x1040 <printf@plt>
   0x0000121c <+95>:    add    esp,0x10
   0x0000121f <+98>:    mov    eax,0x0
   0x00001224 <+103>:   lea    esp,[ebp-0x8]
   0x00001227 <+106>:   pop    ecx
   0x00001228 <+107>:   pop    ebx
   0x00001229 <+108>:   pop    ebp
   0x0000122a <+109>:   lea    esp,[ecx-0x4]
   0x0000122d <+112>:   ret
End of assembler dump.
  • 栈对齐作用:代码确保栈按 16 字节对齐。在操作后,堆栈指针寄存器 esp 的值会小于或等于操作前的值,意味着栈可能会增长,这样可以保护栈上已有的数据
  • 栈对齐原因:在主函数中进行栈对齐,是为了防止函数在栈未对齐的情况下被调用,因为未对齐可能导致程序运行缓慢。虽然 4 字节对齐很重要,但在 x86 架构中,16 字节是缓存行宽度。如果主函数的栈未对齐,那么程序的其余部分也会受影响


下面是我编译的,与图片上一致

pwndbg> disassemble sum
Dump of assembler code for function sum:
   0x0000119d <+0>:     push   ebp
   0x0000119e <+1>:     mov    ebp,esp
   0x000011a0 <+3>:     sub    esp,0x10
   0x000011a3 <+6>:     call   0x122e <__x86.get_pc_thunk.ax>
   0x000011a8 <+11>:    add    eax,0x2e4c
   0x000011ad <+16>:    mov    edx,DWORD PTR [ebp+0x8]
   0x000011b0 <+19>:    mov    eax,DWORD PTR [ebp+0xc]
   0x000011b3 <+22>:    add    eax,edx
   0x000011b5 <+24>:    mov    DWORD PTR [ebp-0x4],eax
   0x000011b8 <+27>:    mov    eax,DWORD PTR [ebp-0x4]
   0x000011bb <+30>:    leave
   0x000011bc <+31>:    ret
End of assembler dump.


  • 代码片段分析:代码是sum函数的汇编实现。push %ebp将基址指针寄存器ebp的值压入栈,mov %esp, %ebp建立新的栈帧,sub $0x10, %esp从堆栈指针寄存器esp中减去 16(十六进制的 0x10),后续指令进行数据移动和加法操作等。
  • 疑问及解释:
    1. “Why?” 标注的sub $0x10, %esp指令,目的是为栈帧分配空间,这可能是编译器为保证栈按字边界对齐而进行的优化操作,通过添加填充字节来实现。
    2. “I only use 4bytes” 标注的mov -0x4(%ebp), %eax指令,虽然只使用 4 字节数据,但整体操作与栈对齐和编译器优化有关。
  • 总结说明:编译器有时会通过添加填充字节优化代码,使其对齐到字边界,理解准确的栈位置需要仔细查看汇编代码 。
  • 函数开始:push %ebp将当前基址指针压入栈,保存上一个函数的栈帧基址。mov %esp, %ebp把当前堆栈指针的值赋给基址指针,建立新的栈帧。sub $0x10, %esp为当前函数的局部变量和临时数据在栈上分配 16 字节空间。
  • 参数传递:
    1. mov 0x8(%ebp), %edx:从相对于基址指针ebp偏移 8 字节的内存位置读取数据,存入edx寄存器,这是获取第一个参数。
    2. mov 0xc(%ebp), %eax:从相对于ebp偏移 12 字节的内存位置读取数据,存入eax寄存器,这是获取第二个参数。在 32 位程序中,函数参数通常按顺序从右向左压入栈,所以从栈中相对ebp的特定偏移处获取参数。
  • 函数运算与返回:add %edx, %eax将两个参数相加。mov %eax, -0x4(%ebp)把相加结果暂存到相对于ebp偏移 -4 字节的栈位置。最后mov -0x4(%ebp), %eax把结果再存入eax寄存器,因为在 32 位程序中,函数返回值一般通过eax寄存器传递 ,随后leave和ret指令用于恢复栈帧并返回。

程序在内存中的完整布局

  • 内核空间(Kernel space):位于内存的高地址部分,大小为 1GB,地址从0xc0000000(即TASK_SIZE)往上。内核空间由操作系统内核使用,用于管理硬件资源、提供系统服务等,用户程序一般不能直接访问。
  • 用户空间(User space):大小为 3GB,从地址 0 到0xc0000000。其中包含以下几个部分:
    • 栈(Stack):在用户空间的较高地址,向下增长。用于存储函数的局部变量、参数和返回地址等,随着函数调用和返回,栈空间动态变化。
    • 内存映射区域(Memory Mapping Region):用于映射文件、共享内存等,在栈和堆之间。
    • 堆(Heap):在用户空间较低地址,向上增长。由程序动态分配内存时使用,如通过malloc等函数申请的内存。
    • BSS 段(BSS segment):存储未初始化的全局变量和静态变量,在程序加载时会被初始化为 0。
    • 数据段(Data Segment):存放已初始化的全局变量和静态变量。
    • 文本段(Text Segment,ELF):处于最低地址,存放程序的可执行代码(机器指令),通常是只读的 。

布局的对应

int a = 0; // .data
int b; // .bss
const double PI = 3.14159f; // .rodata

int main(){
    int c = 321; // stack
    char *d = malloc(16); // heap
    return 0;
}