汇编语言中的数据

在汇编语言中,程序都是由指令流构成的,而指令一般是由操作符和操作数组成的,操作符是CPU用来完成某项功能的操作,而操作数是操作符所处理加工的对象。比如:add eax, 42,add是执行一个加法运算的操作符,是把eax中的值和常数42相加,并保存到eax中,而指令中的操作数,如常数42和存放在eax寄存器中的数值。

在高级语言中,数据本身带有类型信息,比如C语言中,6是一个整型(int)的值,'6’是字符型(char)的值,而"6"就是字符串类型的值,再比如变量:int a,那么在内存地址“&a”中存放的数据是有符号int整型数据。而在汇编语言中,如果脱离开指令上下文环境单独看待它们,它们仅仅是一个二进制的数据,并没有具体的意义,也就是它们没有类型信息,一个数据只有出现在指令中,作为操作数进行操作时才知道它所表示的意义,同一个数据用在不同形式的指令中,可能表示不同的类型。

注,本文以x86-64处理器的汇编语言说明,汇编指令的格式是Intel风格,下面内容均为个人观点。

1、如何表述数据?

既然在汇编语言中,数据自身没有类型信息,那么,如何来表述它们呢?描述数据有三个要素:位置、长度和内容。

位置,就是表示数据存放在哪儿,以及怎么才能找到它?数据只能存放在三个地方:指令中、寄存器中和内存中。

长度,就是数据它占有多大的存放空间。比如数据可以占用1字节、2字节、4字节、8字节、16字节等2的次幂大小的空间,如果是存放在内存中,也可以是3字节、5字节、6字节、乃至n字节大小的空间,比如表示串操作的数组、字符串等数据。

内容,就是指它所表示的是什么。同一个数据可能表示一个字符、整型数、浮点数、地址等,取决于数据用作什么指令的操作数,也就是数据表示什么是根据所处的上下文环境决定的,所表示的是什么和操作它的指令息息相关。

显然,长度和内容对应了高级语言中的类型,比如在C/C++中声明short data,则表示data长度占用2个字节,表示的是范围在-37768~37767之间的整型数,data的位置取决于定义的场合:可以在栈中、堆中,数据区、常量区等。在汇编语言中,如果数据存放在内存中的话,比如[rax],寄存器rax中存放64位的地址,rax可以对应高级语言的void*指针,只是一个位置地址,如果没有相关的前缀信息比如dword ptr,是不知道所指向的数据的长度,也不知道表示的是什么。

下面分别看一下它们在汇编语言中是如何表示的。

2、数据位置-放在什么地方?

在汇编语言中,数据就存在于三个地方:

1、位于指令中
是一个常量,也叫立即数,一般会被编码到指令中,成为指令的一部分。 它只能用作指令的源操作数,比如指令mov eax, 42,源数据42位于指令中。

下面是这条指令编译完之后的二进制指令流,就是这条mov指令汇编完之后的二进制数据b82a000000,在执行时,CPU要把这个二进制指令流放在执行引擎中执行,在译码时就能从中得到数据42。
在这里插入图片描述
我们知道,十进制数字42对应的16进制是0x2a,而b8表示把4字节的二进制数据存入寄存器eax,指令的其余部分2a000000就是数字42,是个4字节长度的数据。

立即数一般来源自高级程序中的const int、constexpr int、整数字面量等,但高级程序中的常量也不一定都会当作立即数位于指令中,有时候也会位于只读数据区,存放于内存中。

2、位于寄存器中
数据存放在通用寄存器中,它可以用作源操作数和目的操作数。比如:

add eax, edx

加法操作add有两个操作数,数据分别存放在寄存器eax和edx中,CPU在执行时,执行引擎会从这两个寄存器中得到数据。

3、位于内存中
指定的是数据在内存中的位置,可以用作源操作数或目的操作数,但不能同时用作源和目的操作数。所在内存位置最常见的表示形式是:[64位寄存器±偏移量],比如[rax+4],寄存器rax的值表示一个内存地址,假设是0x8000000000,把它加上4之后,也就是在0x80000000004内存位置。

3、数据长度-占多大的地方?

数据的长度信息表示,和它存放在哪儿有关,在不同的地方有不同的长度表示方式。

1、立即数
长度取决于字面形式源操作数和目的操作数的长度,一般都是1、2、4、8字节的长度。例如,对于0x42这个立即数,如果目的数是单字节长度,它在指令中是单字节长度,如果目的数是2字节长度,它在指令中的长度是2字节,如果目的数是4、8字节长度,它在指令中的长度是4字节;对于0x4200这个立即数,如果目的数是2字节长度,它在指令中的长度是2字节,如果目的数是4、8字节长度,它在指令中的长度是4字节;对于0x4200000000这个立即数,在指令中只能是8字节长度。

2、寄存器
长度取决于寄存器的大小,就以累加寄存器ax、eax、rax系列以及SIMD寄存器为例:

寄存器 1字节bit长度可能对应的C/C++类型
al18char、unit8_t、int8_t
ax216short、uint_16、int_16
eax432int、uint32_t、int32_t、float
rax864long long、uint64_t, int64_t、double
rdx:rax组合16128struct {int64_t, int64_t}、int64_t[2]、double[2]
ymm16128int8_t[16]、int16_t[8]、int32_t[4]、int64_t[2]、float[4]、double[2]
ymm32256int8_t[32]、int16_t[16]、int32_t[8]、int64_t[4]、float[8]、double[4]
zmm64512int8_t[64]、int16_t[32]、int32_t[16]、int64_t[8]、float[16]、double[8]

即,如果一个数据存放在寄存器eax中,它的长度就是4字节32bit,显然可能是来自高级语言的int、uint32_t、int32_t、float、struct{short, short}, char[4]等类型的数据。

3、内存数据
内存可以存放更多的数据,而且存放数据的地方也没有额外的信息来表示它的长度,在指令中需要专门的标识符来表示长度,在不同风格形式的汇编语言中有不同的表示形式。

在Intel风格的汇编语言中,数据的长度信息需要专门指定前缀,如:单字节长度的内存数据使用byte ptr前缀来指示,4字节长度的内存数据使用dword ptr前缀来表示,例如 mov eax,dword ptr [rax],表示把寄存器rax中的值作为地址,从内存中读取4字节的数据放到寄存器eax中,即数据是[rax]指向的内存位置处连续4字节的长度。

而ATT风格的汇编语言中,长度信息是由操作符使用后缀来指示的,比如:movl (%rax), %eax,使用movl操作符来表示寄存器rax指向的内存位置的4字节数据,把它存入寄存器eax。其它的比如movb、movw、movq等用于不同长度的数据,b、w、l、q后缀分别表示1字节、2字节、4字节和8字节的内存数据的长度。

下面列举各种数据的长度的前缀,以及对应C/C++的类型:

前缀字节bit长度可能对应的C/C++类型
byte ptr18char、unit8_t、int8_t
word ptr216short、uint_16、int_16
dword ptr432int、uint32_t、int32_t、float
qword ptr864long long、uint64_t, int64_t、double
xmm ptr16128int8_t[16]、int16_t[8]、int32_t[4]、int64_t[2]、float[4]、double[2]
ymm ptr32256int8_t[32]、int16_t[16]、int32_t[8]、int64_t[4]、float[8]、double[4]
zmm ptr64512int8_t[64]、int16_t[32]、int32_t[16]、int64_t[8]、float[16]、double[8]

此外,汇编中还有串操作(对应于C/C++语言中的数组类型),这些数据全部存放在内存中,串操作数据的长度,需要由专门的寄存器存放,存放在寄存器cx/ecx中。

4、数据内容- 存放的是什么?

数据所表示的类型,取决于用做什么指令的操作数。指令运算时的它眼中的操作数可以是整型、浮点型、指针、位域、BCD整型、串型(string)和组合类型。

比如,以下面的指令形式为例:
#操作符# eax, dword ptr [rbp-24];
源操作数: dword ptr [rbp-24];
位置:rbp-24指向的内存位置
长度:dword,双字,即4字节长度
类型:取决于操作符的类型,如果是传输型,如mov,不确定,如果是算术运算符,则是整型,如果是逻辑运算型,则是无符号整型
目的操作数:eax
位置:寄存器eax
长度:eax位长是32位,即4字节长度
类型:取决于操作符的类型

下面举一个例子:
假设源数据是qword ptr [rsp],此形式能确定数据的两个要素,位置:rsp指向的内存地址,长度:8字节。下面是它可能出现的场景及类型,假设long、double是64位长度:

MOV rax, qword ptr [rsp]
什么类型都可能,取决于后续以rax为操作数的操作

ADD rax, qword ptr [rsp]
因为ADD操作符用于整型加法运算,因此类型可能是long或unsigned long

MUL rax, qword ptr [rsp]
因为MUL操作符用于无符号整型乘法运算,因此类型明确是unsigned long

IMUL rax, qword ptr [rsp]
因为MUL操作符用于无符号整型乘法运算,因此类型明确是signed long型

MOVSS xmm3, qword ptr [rsp] ;是float类型

MOVSD xmm3, qword ptr [rsp] ;是double类型

MOV rax, qword ptr [rsp]
什么类型都可能,取决于后续以rax为操作数的操作
MOVD xmm0, qword ptr[rax]
后续操作MOVD:qword ptr[rax],rax用作地址,它是指向一个8字节的数据的指针,仍不知指向什么类型。
MOVSD xmm3, xmm0
此时确定是double类型,即rsp是一个二维指针,即对应C/C++的double **ptr类型;

paddw mm1, qword ptr [rsp]; 指向组合整数,等同于C的short[4]数组
paddb mm1, qword ptr [rsp]; 指向组合整数,等同于C的char[8]数组
jmp qword ptr [rsp] ; 是地址,可能是指针,包括函数指针类型
jcc qword ptr [rsp] ; 是地址,可能是指针,包括函数指针类型
call qword ptr [rsp] ; 是函数指针

可见,同一个数据在不同形式的指令中,可能表示不同的类型,比如,假设rsp寄存器指向的内存处,长度8字节的内容是0x8123456781234567。比如,在下面的指令中,它会表示不同的类型:
MUL rax, qword ptr [rsp] ; 是uint64_t类型,值是9305357565928097127
IMUL rax, qword ptr [rsp] ; 是int64_t类型,值是-9141386507781454489
MOV rax, qword ptr [rsp] 和 MOV mm0, qword ptr[rax]; 是指针,指向0x8123456781234567内存位置
MOV mm0, qword ptr[rax] ; 是char数组,值是{0x81, 0x23, 0x45, 0x67, 0x81, 0x23, 0x45, 0x67}
MOVSD xmm0, qword ptr [rsp] ; 是double类型,值是-3.5127004713770933e-303,几乎是0.0。

5、如何访问数据

知道数据在哪儿,也知道长度,如何取获取数据呢?尤其是在内存中的数据。是由不同的寻址方式来获取数据,在x86-64处理器中共有下面几种方式:

1、立即数寻址
数据是立即数常数,存在于指令中。比如指令:mov eax, 42,它的指令二进制编码是:b82a000000,其中4字节长度的2a000000,就是立即数42。

C/C++的代码中,这些数据一般对应字面量或者const、constexpr修饰的常量数据。

2、寄存器寻址
数据在寄存器中,这种寻址方式性能很高,因为寄存器都在CPU执行单元内部,访问它们很快。

在C/C++代码中,函数传递参数、返回结果、循环变量、使用register修饰的变量等都是存放在寄存器中,此外,表达式运算产生的中间结果一般也放在寄存器中。

3、内存直接寻址
数据在内存中,直接使用地址的绝对值访问它们,如mov eax, dword ptr [0x7fff324ce00c],从内存位置0x7fff324ce00c处读取4字节的数据。

在C/C++代码中,全局变量、静态变量、函数入口地址可能会用这种寻址方式,但是因为指令编码较长,可能会用下面的内存间接寻址方式,尤其是x86-64处理器的rip相对寻址方式。

4、内存间接寻址
这一类寻址方式最为丰富。

4.1、寄存器间接
形式:[寄存器],如[rax]

在C/C++代码中,指针使用*操作符解引用、通过this指针访问虚函数表地址时,都是这种形式的寻址。

4.2、基址+偏移量
形式:[寄存器±立即数偏移量],如[rbp-16]

也可以把寄存器间接寻址看作是这种寻址方式当偏移量为0时的特例,如[rax]即为[rax+0]
在C/C++中,访问局部变量、数组元素或者结构成员时常见这种寻址方式,常用于基址的寄存器多是rax、rbx、rbp、rsp,使用它们指令编码较短,在C++中,如果出现rdi作为基址寄存器,很可能是通过this指针在访问对象的数据成员。其中寄存器rbp用作访问函数栈帧的基址、rsp用作栈顶基址,差不多是所有编译器的标准用法了。

4.3、基址+变址
形式:[基址寄存器±变址寄存器] ,如[rbx+rsi]

用作基址寄存器常见有rbx、rbp,变址寄存器常见有rsi、rdi,当然所有寄存器都可以用作它们。

在C/C++中,使用下标变量来访问char、uint8_t等单字节数据组成的数组时,就是这种寻址方式,其中数组起始位置是基址寄存器,而下标是变址寄存器。

4.4、基址+变址因子
形式:[基址寄存器±变址寄存器
立即数因子] ,立即数因子只能是2的次幂,如[rbx+rsi*4]

在C/C++中,使用下标变量来访问short、int、float等多字节数据组成的数组时,就是这种寻址方式,其中数组起始位置是基址寄存器,而下标是变址寄存器,立即数因子就是数组元素的长度,比如对于数组:int ar[],立即数因子就是sizeof(int)=4。

4.5、基址+变址因子+偏移量
形式:[基址寄存器±变址寄存器
立即数因子±立即数偏移量] ,立即数因子只能是2的次幂,如[rbx+rsi*4]。

在C/C++中,使用下标变量来访问结构数组中某个结构中的数据成员,就是这种寻址方式,其中数组起始位置是基址寄存器,而下标是变址寄存器,因子就是结构的长度,偏移量是数据在结构中的偏移量。

4.6、基址+变址+偏移量
形式:[基址寄存器±变址寄存器±立即数偏移量] ,如[rbx+rsi+8]

在C/C++中,使用下标变量来访问结构数组中某个结构的数据成员,如果没有使用4.5的寻址方式(因为变址因子只能使用1、2、4、8等有限的几个值),就使用两条指令来寻址,先计算出变址*因子的值放在变址寄存器中,其中数组起始位置是基址寄存器,而下标位置就是变址寄存器,偏移量是数据在结构中的偏移量。

比如:

// size是16字节
struct pointer {int x;  // 偏移量0int64_t z; // 偏移量8
};long foo(pointer a[], int x) {return a[x].z;
}

可以这样寻址:

foo(pointer*, int):movsx   rsi, esisal     rsi, 4 ; 单独计算变址的值mov     rax, QWORD PTR [rsi+8+rdi]ret

也可以:

foo(pointer*, int):movsx   rsi, esiadd     rsi, rsi ; 翻倍,这样rsi*8就是rsi*16mov     eax, DWORD PTR [rdi+rsi*8+8]ret     0

4.7、rip间接寻址
形式:[rip±立即数偏移量],比如[rip - 0x33538]

现代操作系统中,进程的内存布局模型都是flat平坦模式,即所有的代码和数据都不再分段管理,而是在同一个地址空间内统一编址,这样就使用指令指针寄存器rip来访问数据,在指令访问内存数据时,所执行的位置处(即RIP的值)加上和目标地址的距离——偏移量,就能访问到目标地址处的内存数据。在C、C++访问全局变量、static变量、动态加载的函数和全局变量时,都可以使用这种方式来寻址。

最后

虽然在前面我把数据分成了三部分进行说明,实际上它们三者是同时密切联系在一起的。在一条指令中,同时表示了这三部分的信息,比如下面的指令:

add   edx, DWORD PTR [rdi+4+rsi*8]

1、源数据位于内存中,是一个“基址+变址*因子+偏移量”的寻址方式,位置是经过基址寄存器rdi和变址寄存器rsi运算后的内存地址,数据长度是4字节(dword ptr指示),而地址数据存放在寄存器rdi中,它的长度是8字节,相当于rdi对应C/C++中的指针类型;

2、目的数据位于寄存器edx中,它的长度是4字节

3、指令操作符是add,是算数加法操作,因此源数据和目的数据都是整型数,但是有符号还是无符号数仍不知道。

差不多是类似于下面形式的C/C++代码结构:

struct strct {int x;int z;
};.....
strct ar[M];
int index;
int sum;
...
sum += ar[index].z;

其中ar是一个结构数组,rdi=&ar,rsi=index,edx=sum。

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

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

相关文章

C++17 信号量模拟实现

C17 信号量模拟实现 一、实现原理 C17 标准库没有原生信号量(C20才有)&#xff0c;但可以通过 std::mutex std::condition_variable 模拟实现。以下是核心逻辑&#xff1a; #include <mutex> #include <condition_variable>class CountingSemaphore { private:…

C++ 网络层接口设计与实现:基于 Socket 编程

在网络编程中&#xff0c;网络层是 OSI 七层模型中负责将数据从源节点传输到目的节点的关键层次。在 C 中&#xff0c;网络层的功能通常通过 Socket 编程接口来实现。Socket 提供了一种抽象机制&#xff0c;允许应用程序通过网络发送和接收数据。本文将详细介绍如何在 C 中使用…

uniapp中uni-easyinput 使用@input 不改变绑定的值

只允许输入数字和字母 使用input 正则replace后赋值给A 遇到问题: 当输入任意连续的非法字符时, 输入框不变. 直到输入一个合法字符非法字符才成功被过滤. <uni-forms-item label"纳税人识别号" name"number"><uni-easyinput v-model"numb…

Docker安装hoppscotch

Docker安装hoppscotch 1 简介 1.1 Hoppscotch‌系统 ‌Hoppscotch‌是一个轻量、高效的[API开发生态系统&#xff0c;开源于2020年&#xff0c;原名Postwoman&#xff0c;后更名为Hoppscotch。它基于Node.js构建&#xff0c;支持多种HTTP请求方法&#xff0c;包括GET、POST、…

1.Axum 与 Tokio:异步编程的完美结合

摘要 深入解析 Axum 核心架构与 Tokio 异步运行时的集成&#xff0c;掌握关键原理与实践技巧。 一、引言 在当今的软件开发领域&#xff0c;高并发和高性能是衡量一个系统优劣的重要指标。对于 Web 服务器而言&#xff0c;能够高效地处理大量并发请求是至关重要的。Rust 语言…

CSS伪元素

伪元素 伪元素 用于在元素的内容前后或特定部分插入虚拟元素&#xff0c;并为其添加样式&#xff0c;无需修改 HTML 结构。 语法&#xff1a;使用双冒号 ::&#xff08;现代规范&#xff09; 以下是一些常见的CSS伪元素的示例&#xff1a; 1.::before &#xff1a; 在元素内…

easyexcel使用模板填充excel坑点总结

1.单层map设置值是{属性}&#xff0c;那使用两层map进行设置值&#xff0c;是不是可以使用{属性.属性}&#xff0c;以为取出map里字段只用{属性}就可以设置值&#xff0c;那再加个.就可以从里边map取出对应属性&#xff0c;没有两层map写法 填充得到的文件打开报错 was empty (…

在Ubuntu服务器上部署xinference

一、拉取镜像 docker pull xprobe/xinference:latest二、启动容器&#xff08;GPU&#xff09; docker run -d --name xinference -e XINFERENCE_MODEL_SRCmodelscope -p 9997:9997 --gpus all xprobe/xinference:latest xinference-local -H 0.0.0.0 # 启动一个新的Docker容…

三周年创作纪念日

文章目录 回顾与收获三年收获的五个维度未来的展望致谢与呼唤 亲爱的社区朋友们&#xff0c;大家好&#xff01; 今天是 2025 年 4 月 14 日&#xff0c;距离我在 2022 年 4 月 14 日发布第一篇技术博客《SonarQube 部署》整整 1,095 天。在这条创作之路上&#xff0c;我既感慨…

Redis——五种数据类型

目录 前言 1.String 1.1RAW编码 1.2EMBSTR编码 1.3 INT编码 2.List 3.Set 3.1 InSet编码转化成Dict编码 4.ZSet 4.1结合SkipList和HT实现 4.2使用ZipList实现 4.3编码转换 4.4 ZipList排序功能 5.Hash 5.1Hash底层存储结构 6.Redis数据结构和数据类型关系图 前言…

zookeeper启动报错have small server identifier

解决方案&#xff1a; 1、查看myid是否有重复 2、查看server.X 与myid的X是否一致 3、启动顺序为myid从小到大的服务器顺序

#Linux动态大小裁剪以及包大小变大排查思路

1 动态库裁剪 库分为动态库和静态库&#xff0c;动态库是在程序运行时才加载&#xff0c;静态库是在编译时就加载到程序中。动态库的大小通常比静态库小&#xff0c;因为动态库只包含了程序需要的函数和数据&#xff0c;而静态库则包含了所有的函数和数据。静态库可以理解为引入…

消息队列生产者投递的高可靠性与一致性保障方案

在构建高可靠分布式系统时&#xff0c;确保业务数据库与消息队列&#xff08;MQ&#xff09;之间的一致性是一项核心挑战。尤其当使用 Kafka 作为消息队列中间件时&#xff0c;如何避免“数据库写入成功&#xff0c;但消息发送失败”或“消息重复发送”等问题&#xff0c;成为系…

Formality:Bug记录

相关阅读 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 本文记录博主在使用Synopsys的形式验证工具Formality中遇到的一个Bug。 Bug复现 情况一 // 例1 module dff (input clk, input d_in, output d_out …

通信算法之267 : DJI无人机 云哨 DroneID 640ms

DJI 无人机 与DroneID 转 *** 载 0x01 摘要 消费级无人机可以用于高级航拍、物流和人道主义救援等等。但是其广泛使用给安全、安保和隐私带来了许多风险。例如&#xff0c;攻击方可能会使用无人机进行监视、运输非法物品&#xff0c;或通过侵入机场上方的封闭空域造成经济损…

论坛测试报告

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

npx 的作用以及延伸知识(.bin目录,npm run xx 执行)

文章目录 前言原理解析1. npx 的作用2. 为什么会有 node_modules/.bin/lerna3. npx 的查找顺序4. 执行流程总结1&#xff1a; 1. .bin 机制什么是 node_modules/.bin&#xff1f;例子 2. npx 的底层实现npx 是如何工作的&#xff1f;为什么推荐用 npx&#xff1f;npx 的特殊能力…

【c语言】深入理解指针3——回调函数

一、回调函数 回调函数&#xff1a;通过函数指针调用的函数. 当把一个函数的地址传递给另一个函数&#xff0c;通过该地址去调用其指向的函数&#xff0c;那么这个被调用的函数就是回调函数. 示例&#xff1a; 在【深入理解指针2】中结尾写了用函数指针实现计算器的功能&#…

HTTP 核心概念

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

VidBot:从野外 2D 人体视频中学习可泛化的 3D 动作,实现零样本机器人操控

25年3月来自慕尼黑工大、瑞士 ETH 和微软的论文“VidBot: Learning Generalizable 3D Actions from In-the-Wild 2D Human Videos for Zero-Shot Robotic Manipulation”。 未来的机器人被设想为能够执行各种家务的多功能系统。最大的问题仍然是&#xff0c;如何在尽量减少机器…