寄存器小记

基本程序运行寄存器

  • 通用寄存器:32 位,8 个
  • 段寄存器:16 位,6 个
  • 程序状态与控制寄存器:32 位,1 个
  • 指令指针寄存器:32 位,1 个

在寄存器名称缩略语之前添加字母 E(Extended),表示该寄存器在 16 位 CPU 时就已经存在,并且其大小在 IA-32 下由原 16 位扩展为 32 位;

通用寄存器

通用寄存器是一种通用型的寄存器,用于传送和暂存数据,也可以参与算术逻辑运算,并保存运算结果;

IA-32 中每个通用寄存器的大小都是 32 位,即 4 个字节,主要用来保存常量与地址等,由特定汇编指令来操作特定寄存器;

通用寄存器拆分

为了实现对低 16 位的兼容,各寄存器又可以分为高、低几个独立寄存器;

以 EAX 为例(不涉及大、小端序),只为直观:

  • EAX = 12345678 (0 ~ 31)32 位
  •   AX =        5678 (0 ~ 15)EAX 的低 16 位
  •   AH =        56    (8 ~ 15)AX 的高 8 位
  •   AL =            78 (0 ~ 7)AX 的低 8 位
  • 以上拆分方式适用于 EAX / EBX / ECX / EDX
  • EBP / ESP / ESI /EDI 只能拆分为 16 位,即 BP / SP / SI / DI

通用寄存器含义

  • EAX:(针对操作数和结果数据的)累加器

  • EBX:(DS 段中的数据指针)基址寄存器

  • ECX:(字符串和循环操作的)计数器

  • EDX:(I/O 指针)数据寄存器

    以上 4 个寄存器主要用于算数运算指令中,常常用来保存常量与变量的值;
    某些汇编指令(MUL、DIV、LODS 等)直接用来操作特定寄存器,执行这些命令后,仅改变特定寄存器中的值;
    EAX 一般用在函数返回值中,所有 Win32 API 函数都会先把返回值保存到 EAX 再返回,如果处理结果的大小超过 EAX 寄存器的容量,其高 32 位将会放到 EDX 寄存器中;

  • EBP:(SS 段中栈内数据指针)扩展基址指针寄存器

  • ESP:(SS 段中栈指针)栈指针寄存器

  • ESI:(字符串操作源指针)源变址寄存器

  • EDI:(字符串操作目标指针)目的变址寄存器

    以上 4 个寄存器主要用于保存内存地址的指针;
    ESP 指示栈区域的栈顶地址,某些指令(PUSH、POP、CALL、RET)可以直接操作 ESP;
    EBP 表示栈区域的基地址,函数被调用时保存 ESP 的值,函数返回时再把值返回 ESP,保证栈不会崩溃,这被称为栈帧技术;
    ESI 和 EDI 与特定指令(LODS、STOS、REP、MOVS 等)一起使用,主要用于内存复制;

大端序与小端序

计算机领域中,字节序是多字节数据在计算机内存中存储或网络传输时各字节的存储顺序,主要分为两大类:小端序(Little endian)和大端序(Big endian);

TYPENameSIZE大端序类型小端序类型
BYTEb1[12][12]
WORDw2[12][34][34][12]
DWORDdw4[12][34][56][78][78][56][34][12]
char []str4[73][74][72][00][73][74][72][00]
  • 数据类型为字节型(BYTE)时,其长度为 1 个字节,无论采用大端序还是小端序,字节顺序都一样;
  • 数据长度为 2 个字节以上(含 2 个字节)时,采用不同字节序保存数据时,形成的存储顺序是不同的;
  • 采用大端序存储数据时,内存地址低位存储数据的高位,内存地址高位存储数据的低位,这是一种最直观的字节存储顺序;
  • 采用小端序存储数据时,内存地址高位存储数据的高位,内存地址低位存储数据的低位,这是一种逆序存储方式,保存的字节顺序被倒转;
  • 字符串被保存在字符数组中;
  • 字符数组在内存中是连续的,所以,无论采用大端序还是小端序,存储顺序都相同;
  • 字符串最后以 NULL 结尾;
  • Intel x86 CPU 采用小端序存储数据;
  • 采用大端序保存多字节数据非常直观,常用于 RISC 系列的 CPU 中;
  • 网络协议中通常也采用大端序方式传输数据;

段寄存器

IA-32 的保护模式中,段是一种内存保护技术,它把内存划分为多个区段,并为每个区段赋予起始地址、范围、访问权限等,以保护内存;
此外,它还同分页技术一起用于将虚拟内存变更为实际物理内存;
段内存记录在 SDT(段描述符表)中,而段寄存器就持有这些 SDT 的索引;
段寄存器总共由 6 种寄存器组成,分别是 CS、SS、DS、ES、FS、GS,每个寄存器大小为 16 位,即 2 个字节;;
另外,每个段寄存器指向的段描述符与虚拟内存结合,形成一个线性地址,借助分页技术,线性地址最终被转换为实际的物理地址;
不使用分页技术的操作系统中,线性地址直接变为物理地址;

段寄存器含义

  • CS:Code Segment,代码段寄存器;

  • SS:Stack Segment,栈段寄存器;

  • DS:Data Segment,数据段寄存器;

  • ES:Extra(Data)Segment,附加(数据)段寄存器;

  • FS:Data Segment,数据段寄存器;

  • GS:Data Segment,数据段寄存器;

    CS 寄存器用于存放应用程序代码所在段的段基址;
    SS 寄存器用于存放栈段的段基址;
    DS 寄存器用于存放数据段的段基址;
    ES、FS、GS 寄存器用来存放程序使用的附加数据段的段基址;

  • FS 寄存器从第 18 位开始,存放的是该寄存器从起始位置依次向后的地址,即第 18 位存放的是起始位置的地址;

  • 任意时刻,CS:IP 指向 CPU 将要读取指令的地址,代码段的段地址存放在 CS 中,偏移地址存放在 IP 中;

  • 任意时刻,SS:SP 指向栈顶元素,栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中;

  • 通常,在读写内存单元时,DS 用来存放要访问数据段的段地址;

程序状态与控制寄存器

IA-32 中,标志寄存器的名称为 EFLAGS,其大小为 4 个字节(32 位),由原来的 16 位 FLAGS 寄存器扩展而来;
EFLAGS 寄存器的每位都有意义,每位的值为 1 或为 0,代表 ON/OFF 或 TRUE/FALSE;
其中有些位由系统直接设定,有些位则根据程序命令的执行结果设置;

标志位寄存器

  • ZF 是0 标志符.

    • 当前指令的运算结果为 0,则 ZF 为 1;
  • OF 是溢出标志位;

    • 有符号整数溢出时,OF 为 1;
    • MSB(最高有效位)改变时,OF 为 1;
    • 当指令改变了符号位且返回错误值的时候,OF 为 1,表示溢出成立;
    • 运算结果超出机器能够表示的范围称为溢出;
  • CF 是进位标志符

    • 无符号整数溢出时,CF 为 1;
    • 当指令的无符号运算结果超过最大值时,CF 为 1;
    • 最高位产生进位或借位;
  • PF 是 奇偶标志位

    • 当指令的返回值的二进制表现形式中1的个数为偶数个时,PF 为 1;
    • 运算结果的最低 16 位中含 1 的个数为偶数;
    • 如:11,101,110,1001,1010,1100,1111
  • SF 是符号位标志符;

    • 当指令的运算结果为负数时,SF 为 1;
    • SF 与运算结果的最高位相同;
  • AF 是辅助进位标志符

    • 当指令的运算结果的低 4 位向高 4 位有进位或借位时,AF 为 1;
  • TF 是跟踪标志符

    • 用于调试单步操作;
    • 若 TF 为 1,则每条指令执行结束后,产生中断;
  • DF 是方向标志符

    • 用来控制串处理指令的处理方向;
    • 若 DF 为 1,则串处理中地址自动递减,否则自动自增;
    • 若 DF 为 1,每次操作后使 SI 和 DI 递减,DF 为 0 时则自增;
    • CLD 指令可以将 DF 位置 0;
    • STD 指令可以将 DF 位置 1;
  • IF 是中断标志符

    • 用来控制 CPU 是否响应可屏蔽中断;
    • 若 IF 为 1 则允许中断,否则禁止中断;

指令指针寄存器

  • EIP:指令指针寄存器;

    指令指针寄存器保存着 CPU 要执行的指令地址,其大小为 32 位(4 个字节),由原 16 位 IP 寄存器扩展而来;
    程序运行时,CPU 会读取 EIP 中一条指令的地址,传送指令到指令缓冲区后,EIP 寄存器的值自动增加,增加的大小即是读取指令的字节大小,这样,CPU 每次执行完一条指令,就会通过 EIP 寄存器读取并执行下一条指令;
    与通用寄存器不同,不能直接修改 EIP 的值,只能通过其它指令间接修改,这些特定指令包括 JMP、JCC、CALL、RET;
    还可以通过中断或异常来修改 EIP 的值;

栈是定义在进程中的一段内存空间,向下(低地址方向)扩展,且其大小被记录在 PE 头中,也就是说,进程运行时确定栈内存的大小;

栈通常用于存储局部变量、传递函数参数、保存函数返回地址等;

栈内存在进程中的作用:

  • 暂时保存函数内的局部变量;
  • 调用函数时传递参数;
  • 保存函数返回后的地址;

栈是一种数据结构,它按照 FILO(First In Last Out,先进后出)的原则存储数据;

  • 一个进程中,栈顶指针(ESP)初始状态指向栈底端;
  • 执行 PUSH 压栈命令时,栈顶指针就会向上移动到栈顶端;
  • 执行 POP 弹栈命令时,若栈为空,则栈顶指针重新移动到栈底端;
  • 栈是一种由高地址向低地址扩展的数据结构;
  • 栈是由下向上扩展的,即,栈是逆序扩展的;

向栈压入数据时,栈指针减小,向低地址移动;从栈中弹出数据时,栈指针增加,向高地址移动;

函数调用约定

函数调用约定(Calling Convention):是对函数调用时如何传递参数的一种约定;

  • 调用函数前,要先把参数压入栈,然后再传递给函数;
  • 栈内存是固定的,ESP 用来指示栈的当前位置,若 ESP 指向栈底,则无法再使用该栈;
  • 函数调用后如何处理 ESP 就是函数调用约定要解决的问题;

主要的函数调用约定:

  • cdecl:是主要在 C 语言中使用的方式,调用者负责处理栈;

    如:

    1
    2
    CALL 00401000       ; 调用函数
    ADD ESP, 8 ; 清理栈

    调用函数后,使用ADD ESP, 8命令整理栈;

    调用者直接清理其压入栈的函数参数,这样的方式即是cdecl

    cdecl方式的好处是,它可以像 C 语言的 printf()函数一样,向被调用函数传递长度可变的参数,这种长度可变的参数在其它调用约定中很难实现;

  • stdcall:常用于 Win32 API,该方式由被调用者清理栈;
    若想在 C 语言中使用,只要使用 _stdcall 关键字即可;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include stdio.h

    int _stdcall add(int a, int b)
    {
    return (a + b);
    }
    int main(int argc, char* argv[])
    {
    return add(1, 2);
    }

    调试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    00401000 PUSH EBP                       ; add() 函数
    00401001 MOV EBP, ESP ; 函数栈帧
    00401003 MOV EAX, DWORD PTR SS:[EBP+8] ; 参数 2 拷贝到 EAX
    00401006 ADD EAX, DWORD PTR SS:[EBP+C] ; 参数 1 加上参数 2,结果保存在 EAX
    00401009 POP EBP ; 恢复函数执行前的 EBP
    0040100A RETN 8 ; 恢复函数执行前的 ESP
    0040100D INT3
    0040100E INT3
    0040100F INT3
    00401010 PUSH EBP ; main() 函数
    00401011 MOV EBP, ESP ; 函数栈帧
    00401013 PUSH 2 ; 参数 2 压栈
    00401015 PUSH 1 ; 参数 1 压栈
    00401017 CALL 00401000 ; 调用 add() 函数
    0040101C POP EBP ; 恢复函数执行前的 EBP
    0040101D RETN

    栈的清理工作由 add() 函数最后的RETN 8命令来执行;
    RETN 8命令的含义为RETN + POP 8 字节,即返回后使 ESP 增加指定大小;
    像这样在被调用者内部清理栈的方式即为 stdcall 方式;
    stdcall 方式的好处是相对于每次调用函数都要使用ADD ESP, xxx 的 cdecl 方式代码尺寸小;
    Win32 API 是使用 C 语言编写的,但使用的是 stdcall 方式,这是为了获得更好的兼容性,使 C 语言以外的其它语言也能直接调用 API;

  • fastcall
    fastcall 方式与 stdcall 方式基本类似,但该方式通常会使用寄存器而非栈内存,去传递那些需要传递给函数的部分参数(前 2 个);
    如:函数有 4 个参数,则前 2 个参数分别使用 ECX 和 EDX 寄存器传递;
    fastcall 相对于 stdcall 速度快,毕竟 CPU 访问寄存器要比内存快得多,但需要额外的系统开销来管理 ECX 和 EDX 寄存器;

  • 不管采用哪种方式,通过栈来传递参数的基本概念是一样的;