汇编基础,寄存器、指令、函数栈(有栈协程)

news/2024/12/19 0:24:23/文章来源:https://www.cnblogs.com/yiwanfengweng/p/18614690

ref

  • 很好的入门视频教程,基础寄存器和基础指令讲得好,https://www.bilibili.com/video/BV12M4m1o7f6
  • 简化了很多细节,但可以粗略入门,https://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
  • 也是一个简化版,但是比上一个详细,https://www.cnblogs.com/alexlance/p/17432758.html
  • 从汇编角度详细讲解函数调用栈和有栈协程,https://www.yigegongjiang.com/2023/stackForFunc/
  • 将各种语言在线转为汇编的工具网站,https://godbolt.org/

前言

本文重在记录基础知识和函数调用栈,并不全面。
认识汇编有两大实用好处:

  • 更好理解CPP的一些概念(例如右值引用等);
  • 方便GDB调试core文件查找非预期bug;

正文

字节序

小端存储,值的低字节部分存放在内存空间的低位,值的高字节部分存放在内存空间的高位。

内存

汇编(或者说计算机地址总线)的寻址基本单位是字节(Byte),所以32位寄存器的寻址空间是2的32次方字节(4GB)(额外提一句,64位寄存器的寻址空间实际上没有2的64次方字节)。

如图所示,栈从高字节向低字节生长,堆从低字节向高字节生长

寄存器

寄存器有四个分类:

  • 通用目的寄存器
  • 段寄存器
  • 标志寄存器
  • 指令指针寄存器

本文关注通用目的寄存器,通用目的寄存器的大部分可以混用。

16位通用目的寄存器

ax, bx, cx, dx, si, di, sp, bp,一共8个:

  • sp(stack pointer)是栈顶指针寄存器,非常重要;
  • bp(pointer to data on the stack)是栈基址指针寄存器,64位里不再需要bp;
  • ax, bx, cx, dx可以只使用其中的高(high)8位或者低(low)8位:
    • ah, al, bh, bl, ch, cl, dh, dl;

32位通用目的寄存器

将16位通用目的寄存器拓展(Extend)到32位,EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP。

64位通用目的寄存器

将32位通用目的寄存器拓展到64位,并额外添加了8个寄存器。
RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, R8, R9, R10, R11, R12, R13, R14, R15。R前缀取自Rigister。

RBP

rbp寄存器用于指向当前栈的基址,实际上也是上一个调用栈的栈顶地址

RSP

rsp寄存器用于指向当前栈顶的地址,实际的汇编语言里可能是出于优化目的rsp不一定始终指向栈顶(在leave指令中有示例解释改现象)

传参常用寄存器

以主调函数调用被调函数func(1,2,3,4,5,6)为例:
主调函数所在的栈依次使用使用RDI、RSI、RDX、RCX、R8、R9寄存器向被调函数的栈传参

        mov     r9d, 6mov     r8d, 5mov     ecx, 4mov     edx, 3mov     esi, 2mov     edi, 1

如果被调函数参数超过6个,以主调函数调用被调函数func(1,2,3,4,5,6,7,8)为例:
主调函数把超出6个的参数按照逆序压入栈中,被调函数使用当前被调函数栈的rbp+正偏移的方式寻址主调函数栈中压栈的函数参数。

        push    7mov     r9d, 6mov     r8d, 5mov     ecx, 4mov     edx, 3mov     esi, 2mov     edi, 1

如果被调函数参数小于6个,但是传递给被调函数的参数是当前主调函数的局部变量,那么被调函数也是通过使用当前被调函数栈的rbp+正偏移的方式寻址主调函数栈中压栈的函数参数

同名寄存器的关系

同名寄存器如RAX EAX AX AH AL是同属一个寄存器RAX。

RAX, RBX, RCX, RDX可以取低16位的高8位和低8为,其他12个寄存器只能取低8位、低16位、低32位和64位:

  • r8b表示低8位,r8w表示低16位,r8d表示低32位;
  • 1 word = 2 byte
  • 1 double word = 4 byte

段寄存器

段寄存器CS, DS, SS, ES, FS, GS,每个都保存着16位段选择子(这词难懂,不用在意),用于标识内存中特定的段。其中CS指向代码段,SS指向栈段, DS, ES, FS, GS指向数据段。

内存被分为不同的段,通过访问段基址+偏移方式访问内存,注意内存物理上连续,分段是CPU寻址方式。

16位8086CPU的地址总线是20位,但寄存器和数据总线是16位。为了不浪费多余的4位地址总线便引入段概念来寻址:段地址*16 + 偏移地址。

32位8086CPU数据总线和地址总线位数一致,分段寻址不再必要,段的作用改成是施加内存保护,并引入内存分页机制、虚拟内存等概念,统称保护模式。(出于向下兼容, 16位寻址方式保留,称作实模式,提供直接访问物理内存的手段)

64位8086CPU下,段概念继续弱化,内存变成平坦模式,即无段式内存,所有对内存的访问都在同一个地址空间进行,段保护被弱化,强调对页的保护。

标志寄存器

不感兴趣。

指令指针寄存器

指令指针寄存器存储的是CPU即将执行的下一条指令的地址,用ip/eip/rip指代(16, 32, 64位)

指令

汇编指令有两种风格:intel风格和AT&T风格。

  • intel风格典型风格是目标操作数在左,源操作数在右
    • 例1,mov eax, ebx,是把ebx的值赋值给eax
    • 例2,add eax, ebx,是把eax和ebx的值相加赋值给eax
    • 例3,MOV RAX, QWORD PTR DS:[RBX+0X10] ,访问RBX+0x10的内存地址上的16字节的值,并写入到RAX
  • AT&T风格是目标操作数在右,源操作数在左
    • 例1,mov %eax, %ebx,是把eax的值复制给ebx
    • 例2,sub $0x8, %rsp, 把rsp的值减去16进制立即数0x8
    • 例3,sub 8, %rsp, 把rsp的值减去10进制立即数8
    • 例4,sub $8, %rsp, 同上,把rsp的值减去10进制立即数8
    • 例5, movl -0x50(%rbp), %eax, 从 rbp 寄存器指向的内存地址加上 -0x50 偏移量(负号表示减)的地方读取一个 32 位的值,并将其存储到 eax 寄存器中,movl的l表示这是一个 32 位操作。

MOV指令

此小节主要使用intel风格示例讲解MOV指令访问寄存器和内存地址的规则和限制,比较琐碎,可以不看。

使用立即数给寄存器赋值

MOV RCX, 0xFFFFFFFFFFFFFFFF   // 给寄存器RCX赋值立即数,立即数值为0xFFFFFFFFFFFFFFFF,RCX会变成0xFFFFFFFFFFFFFFFF// 严格来说立即数应该用FFFFFFFFFFFFFFFFh来表示,这里使用0xFFFFFFFFFFFFFFFF只是方便阅读
MOV ECX, 0xFFFFFFFFFFFFFFFF   // 报错,ECX是32位,立即数64位,超限
MOV ECX, 0xFFFFFFFF                 // 给寄存器ECX赋值立即数,立即数值为0xFFFFFFFF,RCX会变成0x00000000FFFFFFFF// 给64位寄存器的低32位整体赋值时,高32位会被清0,如果给给64位寄存器的低8位赋值时,高32位不会被清0
MOV ECX, 0xFFFFFFFF                 // 给低32位赋值,RCX值为0x00000000FFFFFFFF,高位会被置零

寄存器相互赋值,只有ABCD之间可以高低错位赋值,其他寄存器之间不能错位赋值

MOV SPL, BPL                  // 把RBP的低8位赋值给RSP的8位
MOV AH, CL                     // 把RCX低8位赋值给RAX低16位里的高8位
MOV AH, CH                     // 把RCX低16位里的高8位赋值给RAX低16位里的高8位
MOV AL, R8B                    // 把R8低8位赋值给RAX低8位
MOV AH, R8B                    // 报错,只有ABCD四个寄存器可以相互之间高低位错位赋值
MOV RCX ECX                  // 报错,两个寄存器小不匹配,寄存器之间赋值必须大小匹配
MOV EAX, EAX                  // 看似没有做什么,实际上RAX的高32位被清0了
MOV AH, SPH                    // 错误,除了ABCD可以访问低16位的高8位,其他寄存器都不可以

寄存器和寄存器指向的内存单元之间相互赋值

MOV EAX, DWORD PTR DS:[RCX]     // 把RCX指向的内存地址里的4字节值赋值给EAX,RAX的高32位会被清0
MOV QWORD PTR DS:[RCX], RDX     // 把RDX的8字节的值赋值给RCX指向的内存地址
MOV QWORD PTR [RCX], RDX          // 默认也是寻址DS段的内存地址,把RDX的8字节的值赋值给RCX指向的内存地址

使用偏移来访问内存单元

MOV RAX, QWORD PTR DS:[RBX+0X10]           //访问RBX+0x10的内存地址
MOV QWORD PTR DS:[RBX+0X10], RAX           //访问RBX+0x10的内存地址
MOV QWORD PTR DS:[RBX+RAX+0x10], RCX   //访问RBX+RAX+0x10的内存地址
MOV QWORD PTR DS:[RAX+RCX*8+0x10], R10 //访问RAX+RCX*8+0x10的内存地址

只有RAX(EAX AX AH AL)可以给直接内存地址赋值,其他寄存器只能访问寄存器指向的内存地址

MOV RAX, QWORD PTR DS:[0x23878AE00000] //把直接的内存地址0x23878AE00000上16字节的值赋值给RAX
MOV DWORD PTR DS:[0x23878AE00000], EAX //把EAX的4字节的值赋值给直接的内存地址0x23878AE00000上,RAX高32位清0
MOV RBX, QWORD PTR DS:[0x23878AE00000] //报错,只有RAX(EAX AX AH AL)可以和直接和直接的内存地址相互赋值,// 其他的寄存器需要通过寄存器来指向内存地址来相互赋值//换句话说只有0号寄存器可以直接和内存地址通信
MOV QWORD PTR DS:[0x23878AE00000], RBX //报错,原因同上

立即数只能给32位的内存地址单元赋值

MOV [0x23878AE00000] 0x45464154 //报错,立即数的大小是ambiguous不确定的
MOV DWORD PTR DS:[0x23878AE00000] 0x45464154 //报错,不可以直接对64位的地址赋值立即数
MOV QWORD PTR DS:[0x23878AE00000] 0x45464154 //报错,原因同上
MOV QWORD PTR DS:[0x12345678] 0x45464154     //给内存地址0x12345678赋值一个16字节大小的值45464154
MOV RAX 0x23878AE00000
MOV QWORD PTR DS:[RAX], 0X1   //如果要把立即数赋值给64位内存地址,需要使用寄存器中转内存地址

PUSH/POP

PUSH

push 源操作数

push指令先把rsp寄存器值减去操作数的字节数,以提高栈顶,然后使用mov指令把操作数赋值给0x0(%rsp), 假设操作数大小时8 Byte,作用同下:

sub $8, %rsp
mov 源操作, 0x0(%rsp)

push指令有3个作用:

  • 保存寄存器的值,函数调用时,使用push指令将当前寄存器值压入栈保留;
  • 参数传递,函数参数可以通过栈传递,当被调用的栈通过RBP+偏移的方式寻址到在上一个栈中入栈的函数参数;
  • 保存返回地址,CALL指令会自动地把下一条指令的地址(返回地址,即RIP的值)压栈;

POP

pop 目标操作数

pop指令先把,然后使用mov指令把0x0(%rsp)地址上的值赋值给目标操作数,然后给rsp加上操作数的字节数, 假设操作数大小时8 Byte,作用同下:

mov 0x0(%rsp), 目标操作数
add $8, %rsp

pop指令有3个作用:

  • 恢复寄存器的值,函数返回时使用pop指令将此前保存的寄存器值出栈,恢复寄存器原值;
  • 恢复返回地址,函数执行完毕后,ret指令回隐式地把返回地址pop出栈到RIP寄存器;

CALL/RET

CALL

call 目标地址

call指令用于调用函数,作用是把rip的值(被调函数的返回地址)压栈,并把rip更新为目标地址的值。

RET

ret

ret指令作用是把被压栈的原rip的值(被调函数的返回地址)出栈到rip寄存器。

LEAVE

leave

leave放在ret指令之前, 用于调整rsp和 rbp,作用同下:

mov %rbp, %rsp //将rbp的值赋值给rsp,rsp恢复主调函数的栈顶
pop %rbp           //出栈主调函数的栈基址到rbp,注意这条指令实际上也给rsp做了改动

leave指令不是必须要出现在ret指令之前的,rsp的值也不是一定要指向栈顶的,如下所示

int sum2(int a, int b) { // sum2函数没有leave, 因为sum2没有局部变量,因此sum的栈是空的// 所以ebp和esp值是相等的,无须使用leave把rbp的值赋值给rsp,直接使用pop rbp恢复上一个栈的基址即可return a + b;
}
int main(void) {// main函数没有leave,即使main函数有局部变量,而且main函数的栈也久了这个局部变量//但是出于优化并没有修改rsp的值,所以rsp和rbp的值是相等的无需leaveint size_long = sizeof(long);//int p = sum2(1,2);return 0;
}sum2(int, int):push    rbpmov     rbp, rspmov     DWORD PTR [rbp-4], edimov     DWORD PTR [rbp-8], esimov     edx, DWORD PTR [rbp-4]mov     eax, DWORD PTR [rbp-8]add     eax, edxpop     rbpret
main:push    rbpmov     rbp, rspmov     DWORD PTR [rbp-4], 8mov     eax, 0pop     rbpret

函数调用栈


如图所示,从上往下依次是内核区、栈区、共享数据区 (动态库)、堆区、数据区、代码区、留置区。这些区域的划分都是虚拟内存,并非物理内存。越往上内存地址越大,越往下内存地址越小,本小节研究函数调用栈(栈区)。
上图的栈的组成分别是:上个调用栈RBP、留置内存区域、局部变量、下个调用栈的参数(优先使用RDI RSI RDX RCX R8 R9传参) 和 当前调用栈的下一个指令执行地址即RIP值 组成。
让人疑惑的留置内存区域究竟是什么?实际上是栈是按照16字节的整数倍来给局部变量预留栈的局部变量空间的,尚未使用的栈空间就是留置内存区域,而函数参数(在函数参数实际上有两种,一是当前栈向下一个栈传递的超过6个之外的参数需要当前栈空间存放,二是当前栈需要开辟空间存放上一个栈通过寄存器传递给当前栈的实参)则是占据多少字节就预留多少栈空间,使用https://godbolt.org/查看汇编代码可以自行验证,如下是验证中的一个示例(具体看汇编的注释):

int mul(int x, int y) {int sum = x + y;return sum;
}
int sum(int a, int b, int c, int d, int e, int f, int g, int h) {int s = mul(a, b);int div = g / h;int sub = a - b;int t = s + c + d + e + f + g + h;int res = t + s;int useless1 = 0;int useless2 = 0;return res;
}
int main(void) {int useless1 = 0;int useless2 = 0;int useless3 = 0;int useless4 = 0;int useless_sum = useless1 + useless2 + useless3 + useless4;const int size_int = sizeof(int);const int size_long = sizeof(long);const int size_longlong = sizeof(long long);int p = sum(1,2,3,4,5,6,7,8);return 0;
}mul(int, int):push    rbpmov     rbp, rspmov     DWORD PTR [rbp-20], edimov     DWORD PTR [rbp-24], esimov     edx, DWORD PTR [rbp-20]mov     eax, DWORD PTR [rbp-24]add     eax, edxmov     DWORD PTR [rbp-4], eaxmov     eax, DWORD PTR [rbp-4]pop     rbpret
sum(int, int, int, int, int, int, int, int):push    rbpmov     rbp, rspsub     rsp, 56 // sum有7个局部变量,所以需要预分配32Byte的栈空间// sum需要在栈上保存6个寄存器传递的实参,需要6*4=24Byte的栈空间,所以一共是32+24=56mov     DWORD PTR [rbp-36], edi // 在栈上存放上一个栈通过寄存器传递的实参amov     DWORD PTR [rbp-40], esimov     DWORD PTR [rbp-44], edxmov     DWORD PTR [rbp-48], ecxmov     DWORD PTR [rbp-52], r8dmov     DWORD PTR [rbp-56], r9d // 在栈上存放上一个栈通过寄存器传递的实参fmov     edx, DWORD PTR [rbp-40]mov     eax, DWORD PTR [rbp-36]mov     esi, edxmov     edi, eaxcall    mul(int, int)mov     DWORD PTR [rbp-4], eaxmov     eax, DWORD PTR [rbp+16] // 访问上一个栈传给当前栈的实参gcdqidiv    DWORD PTR [rbp+24] //访问上一个栈传给当前栈的实参hmov     DWORD PTR [rbp-8], eaxmov     eax, DWORD PTR [rbp-36]sub     eax, DWORD PTR [rbp-40]mov     DWORD PTR [rbp-12], eaxmov     edx, DWORD PTR [rbp-4]mov     eax, DWORD PTR [rbp-44]add     edx, eaxmov     eax, DWORD PTR [rbp-48]add     edx, eaxmov     eax, DWORD PTR [rbp-52]add     edx, eaxmov     eax, DWORD PTR [rbp-56]add     edx, eaxmov     eax, DWORD PTR [rbp+16]add     edx, eaxmov     eax, DWORD PTR [rbp+24]add     eax, edxmov     DWORD PTR [rbp-16], eaxmov     edx, DWORD PTR [rbp-16]mov     eax, DWORD PTR [rbp-4]add     eax, edxmov     DWORD PTR [rbp-20], eaxmov     DWORD PTR [rbp-24], 0mov     DWORD PTR [rbp-28], 0mov     eax, DWORD PTR [rbp-20]leaveret
main:push    rbpmov     rbp, rspsub     rsp, 48 // main函数一共有9个局部变量,所以会分配(16*3)48个Byte的栈空间// 如果去除一个局部变量,例如int useless_sum,剩下8个局部变量// 此条汇编指令就会变成sub     rsp, 32mov     DWORD PTR [rbp-4], 0mov     DWORD PTR [rbp-8], 0mov     DWORD PTR [rbp-12], 0mov     DWORD PTR [rbp-16], 0mov     edx, DWORD PTR [rbp-4]mov     eax, DWORD PTR [rbp-8]add     edx, eaxmov     eax, DWORD PTR [rbp-12]add     edx, eaxmov     eax, DWORD PTR [rbp-16]add     eax, edxmov     DWORD PTR [rbp-20], eaxmov     DWORD PTR [rbp-24], 4mov     DWORD PTR [rbp-28], 8mov     DWORD PTR [rbp-32], 8push    8 // 参数压栈多余的两个参数,会使得栈继续增长,rsp 需要减去 8个字节push    7 // 汇编里的整形常量的长度取决于sum函数形参类型int, 而int的长度是4个字节mov     r9d, 6mov     r8d, 5mov     ecx, 4mov     edx, 3mov     esi, 2mov     edi, 1call    sum(int, int, int, int, int, int, int, int)add     rsp, 16mov     DWORD PTR [rbp-36], eaxmov     eax, 0leaveret

有栈协程

待老夫学习了协程后写相关总结。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/855112.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

最大流之上下界可行流

一.无汇源上下界可行流#include<bits/stdc++.h>#define x first #define y second #define endl \n #define int long longusing namespace std;const int N=10010,M=200010,INF=1e15;//根据边的大小,来调整N,M,INFint n,m,S,T; int h[N],e[M],f[M],l[M],ne[M],idx;//l数…

项目中ES踩坑记录

当用到script score query 时,出现java 异常 这种异常多半是对检索出来的数据进行script计算的时候出错了,大多数是空指针异常情况。 解决思路是: 1.在query条件中,将需要script计算的字段的数据过滤掉。比如用到了feature字段进行计算的时候,需要保证feature有值并且是512…

从“bug”到“成就感”——软件工程大冒险

从“bug”到“成就感”——软件工程大冒险 这一学期的《软件工程》简直可以称为我的“技术冒险之旅”。从最初的迷茫,到逐渐掌握核心技能,再到团队合作中的互助与共识,到最后顺利完成项目时的“轻舟已过万重山”,经历了从“bug”到“成就感”的转变,既有汗水,也有欢笑。回…

【AI安全漏洞】VLLM反序列化漏洞分析与保姆级复现(附批量利用)

#CVE-2024-9052环境需要 Linux(这里使用kali)、Anaconda首先安装Anaconda 前言 最好使用linux,如果使用windows可能会产生各种报错(各种各种各种!!!),最好使用Anaconda,方便独立管理虚拟机 使用conda创建虚拟机、python要求3.10 conda create -n vllm_beam python=3.…

动态数据源 @DS 注解源码解析

参考:动态数据源切换——@DS 注解源码解析前言 借助 dynamic-datasource 可实现多数据源读写,其核心注解@DS用来动态切换数据源。 下面介绍@DS注解的实现原理。 如何使用 在 pom 中引入依赖: <!-- spring-boot 1.5.x 2.x.x --> <dependency><groupId>com.…

【开源系列】CentOS7下Docker环境搭建开源堡垒机Apache Guacamole

Apache Guacamole 是一个无客户端远程桌面网关。它支持 VNC、RDP 和 SSH 等标准协议。不需要插件或客户端软件。借助 HTML5,一旦在服务器上安装了 Guacamole,只需使用 Web 浏览器即可访问桌面。 1.Guacamole的架构介绍 Guacamole不是一个独立的网络应用程序,而是由多个部分组…

ThreeJs-07操控物体实现家具编辑器

本章节实现效果,通过gui快速添加场景,家具,并且可以快速设置家具实现一个编辑器效果一.基础设置与物体添加列表 用之前做过的一个案例来改首先不要这个模型,然后换个背景颜色,并且添加一个网格辅助器1.1 添加场景 先往事件对象里面添加一个函数,到时候点击就会调用这个函…

数据集划分;参数超参数;交叉验证

在机器学习和深度学习中,将数据分为训练集(Training Set)、验证集(Validation Set)和测试集(Test Set)是常见的做法,每部分数据承担不同的任务: 一.基本概念 1.训练集(Training Set):训练集用于训练模型,即通过算法调整模型的参数以最小化损失函数(Loss Function…

爱米导航网(imi),您的互联网书签搭子

爱米导航(Imi)网是一个综合性的互联网资源聚合平台,它以其丰富的内容和便捷的服务受到了广大用户的喜爱。该网站收录了数千个不同类型的互联网工具网站,覆盖了AI工具、自媒体运营工具、产品经理工具以及UI设计师工具等多个领域,为用户提供了一个一站式的解决方案。 爱米导…

记录一次springboot启动流程不完整版

1.Sort ApplicationInitializer:2.sort Listener:3.getRunListener:4.eventPublishingRunListener.starting 发布启动中事件; 构建 ApplicationArguments 参数, 4.1prepareEnvement: [StubPropertySource {name=servletConfigInitParams}, StubPropertySource {name=servletC…

键盘连击软件解决方案

解决的问题 jjjjjjjjjjjjjjjj键盘连击了,windows系统中的“筛选键”功能就能短暂解决这个问题,可是筛选的时间太长无法在面板设置更短的时间,但可以使用软件解决。 方法一 系统 筛选键 win+i 打开设置 -> 搜索“筛选键” -> 打开筛选键开关 使用限制:重复键最低只能设…

【建议收藏】最新版IDEA2024.3及 AI Assistant 一键激活到2099

成功永久激活 一、支持的IDE和版本支持JB家族所有IDE激活 支持版本为2021.3~2024.1二、如何破解激活 第一步:激活工具下载 为了防止破解工具被删除,通过公众号回复“「永久激活」”获取下载最新工具(如过期,请记得提醒我哦) 关注公众号后台回复“「永久激活」”,获取最新…