汇编程序设计与计算机体系结构软件工程师教程笔记:函数、字符串、浮点运算

2023-11-17

《汇编程序设计与计算机体系结构: 软件工程师教程》这本书是由Brain R.Hall和Kevin J.Slonka著,由爱飞翔译。中文版是2019年出版的。个人感觉这本书真不错,书中介绍了三种汇编器GAS、NASM、MASM异同,全部示例代码都放在了GitHub上,包括x86和x86_64,并且给出了较多的网络参考资料链接。这里只摘记了NASM和MASM,测试代码仅支持Windows和Linux的x86_64。

6. 函数

6.2 栈内存入门:栈内存(stack memory)是为自动变量而设的一块区域(这里的自动变量是指局部变量,或者说非动态的变量)。调用函数的时候,需要用栈来保存函数中的局部变量,而函数结束的时候,则需要弃用这些变量。高级语言的一项特征在于它会自行管理栈内存(这有时也叫做运行时栈或运行期栈),相反,汇编语言不会这样做,而是需要你自己去管理。

与栈内存有关的重要事项:

(1).栈会在调用函数时增长,并在调用结束时收缩。

(2).栈会在创建(或者说推入/压入)局部变量时增长,并在弃用(或者说弹出)局部变量时收缩。

(3).每个进程或线程的栈,其大小受操作系统限制,例如Linux/Mac系统默认是8MB,Windows默认是1MB。

(4).每次调用函数(这也包括调用主函数main(),以及递归地调用自身)都会出现对应的栈帧(stack frame)。它是栈内存中的一块区域,随着函数调用而产生。

(5).栈帧用来保存函数的局部变量。

(6).栈内存是向下增长的,也就是说,推入栈中的新内容会出现在地址值较低的位置中。

(7).32位环境下,栈中的每个元素占据4个字节,64位环境下占据8个字节。

(8).数值默认按照小端序入栈。权重最高的字节保存到地址最高的位置上,权重较低的字节保存到地址最低的位置上。

(9).x86_64的函数都必须按16字节对齐(所有平台都是这样)

6.3 x86与x86_64的调用约定:

调用约定也叫做调用协议,用来确定函数在底层的实现流程,包括如何传递参数、如何管理栈内存,以及如何返回值。

cdecl(32位):cdecl(C declaration)调用约定是32位平台最常见的一种约定,因为它是基于C语言标准而设立的。一般来说,GCC、Clang与Visual Studio的C编译器默认都会使用cdecl约定。有些开发环境(如Visual Studio)允许开发者在项目属性中调整默认的约定方式。cdecl有4项主要特征:

(1).参数按照相反的顺序(也就是从右到左的顺序)入栈。

(2).eax、ecx与edx需要由调用方(caller, 主调方)保存(叫做易失性的寄存器),而其余的通用寄存器则需要由受调方(callee, 被调用方)来保存(叫做非易失性的寄存器)。因此,如果想令eax、ecx及edx寄存器在调用完函数之后还能保持调用之前的值,那么应该在主调函数中先将其保存起来,因为受调函数在执行过程中有可能会修改它们。

(3).在大多数情况下,返回值放在eax寄存器里,如果要返回的是浮点数,那么应该放在st(0)寄存器里。

(4).栈由调用方负责清理。

C语言支持参数数量可变的函数。由于这种函数的参数列表长度不固定,因此,受调方并不清楚自己到底接收了几个参数。于是,就要求栈必须由主调方来清理,而且每次调用的时候都得清理。C语言里的printf()就属于这样的函数。

在32位环境与64位环境下引用局部变量可以采用不同的办法。32位环境下,一般用ebp作为基准点来访问参数,而64位环境下则不太这样使用。其原因有三:第一,64位环境下通常使用寄存器(而不是栈)来传递参数;第二,64位环境下可以使用RIP相对寻址机制;第三,64位环境下,系统默认在栈内存中划分一块暂存空间,使我们可以直接以rsp寄存器为基准来引用局部变量。

stdcall(32位):被Windows API使用,一般来说,它的规则与cdecl是相同的,只是在一个地方有所区别(第4项):

(1).参数按照相反的顺序(也就是从右到左的顺序)入栈。

(2).eax、ecx与edx应该由调用方保存,而其余的通用寄存器则应该由受调方来保存。

(3).在大多数情况下,返回值放在eax寄存器里,如果要返回的是浮点数,那么应该放在st(0)寄存器里。

(4).栈由受调方负责清理。

stdcall的好处是,受调函数可以在运行RET(返回)指令的时候顺便把栈清理干净,从而使主调函数可以少写一些代码。由于清理栈所用的代码是写在受调函数中的,因此,每次调用这个函数时,这些代码都能得到执行,而不用在主调函数里重复一遍。栈能够由受调方清理,是因为stdcall不允许参数个数可变的函数,因此,受调方可以知道自己接收了几个参数,从而正确地把栈清理干净。stdcall的RET指令与cdecl稍有不同,它必须指出栈中有多少个字节的内容需要移除

x86_64(64位):约定试图提升函数的执行速度,它允许程序通过寄存器来传递某些参数,而不一定非要通过栈来做(这与32位模式下的fastcall约定是类似的,fastcall也是一种可以通过寄存器来传递数值的约定方式,它运用在32位环境中)。下表指出了两种64位的函数调用约定所具备的特征:在这两种情况下(Microsoft x64, System V AMD64(AMD64)),如果要传递的参数比参数寄存器的数量还多,那么其余的参数依然遵循从右至左的顺序入栈。

64位模式下的函数必须按16字节对齐。x86_64支持流式SIMD扩展(Streaming SIMD Extensions, SSE),这是一套有助于并行处理的指令。SSE的操作一般都是128位的,SSE操作必须按16字节对齐。为了确保使用SSE指令的函数不会出现栈对齐错误,所有的栈帧都应该向16字节处对齐。此外,由于CALL指令默认会把8个字节的地址推入栈中,因此,在很多情况下,我们都需要再向栈中推入8字节,从而实现16字节对齐

x86_64调用约定还有一个重要特征,就是在栈中预留额外的空间。Microsoft x64约定所预留的空间,叫做shadow space或home space,其长度是32个字节,用来放置参数寄存器中的数值。即便不需要传递任何参数,也依然得预留这么大的空间。如果受调函数经由r8、r9、rcx及rdx寄存器收到的参数还要在该函数调用其它函数的时候用到,那么这些参数的值就可以在shadow space中保存一份,除此之外,这块暂存空间(scratch space)也可以用来做其它的事情。

AMD64调用约定没有32个字节的shadow space这一概念,但它会把存放返回地址所用的位置(rsp就指向此处)下方的128个字节视为red-zone。这是打算留给函数使用的一块临时存储空间,不会被系统侵扰。如果没有PUSH、POP或CALL指令涉及这块区域,那么rsp就不会改变,于是,开发者可以通过rsp来引用局部变量及标识符(例如在实现跳转和循环的时候,就可以这样引用相关的标识符)。预留这块区域是为了令rsp寄存器不会因局部变量而频繁地调整,进而节省时钟周期,以求优化程序的执行效率。此外,它还可以用来实现末端函数(leaf function, 叶函数),也就是那种不会再继续调用其它函数的函数。主调方可以利用red-zone来完成未端函数所要执行的计算,这样就不用再通过CALL指令真的去调用它了,从而可以免去一些开销。与Microsoft x64的shadow space不同,AMD 64的red-zone并不需要由开发者或编译器专门去预留,它仅仅用来表达一项承诺,意思是说:系统信号及中断处理程序是不会触碰rsp下方那128个字节的。但是,如果调用了其它函数,那么主调函数的red-zone就会遭到破坏,因此,这块区域主要还是留给那些不会再调用其它函数的末端函数来用的,那些函数可以把它当做暂存空间。

6.7 重要的寄存器(32位和64位)及命令,如下表所示:

6.9 与平台有关的注意事项:

(1).在Windows系统中编写代码时,应该用PROC与ENDP命令指出例程的起点与终点

(2).在Windows系统中编写MASM汇编代码时,可以通过.MODEL命令指出整个程序所遵循的调用约定(例如C或STDCALL),也可以在每个函数的PROC右侧专门指出该函数所遵循的调用约定,以便更为精细地控制程序。

(3).在Windows系统中编写MASM汇编代码时,可以通过USES命令指出一些寄存器,使得汇编器为它们生成相应的PUSH或POP指令。这些指令会自动出现在函数的开场与收场部分,从而能够在程序进入及退出该例程时得到执行(这些写法对于32位及64位模式都适用)。

7. 与字符串有关的指令及结构体

7.2 辅助指令:

下表列出了两条方向指令,分别用来给方向标志(direction flag)清零及置位。方向标志决定了内存地址在执行字符串指令的过程中是应该自动递增还是自动递减,这实际上也就决定了字符串的处理顺序是从左至右还是从右至左。CLD与STD指令最适合用在重复操作之前,以便将方向标志调整好。

字符串的重复操作是通过重复指令实现的,下表列出了三种形式的REP(repeat)指令,它们都采用C系列的寄存器做计数器。

把调整方向标志的指令与重复指令结合起来,就能够自动处理字符串数据而不用手工编写循环。下面是重复指令运作方式的步骤:

(1).重复指令要判断相关的C系列寄存器是否大于0,如果已经等于0就不再重复。若是比0大则继续重复。

(2).执行字符串指令,根据结果设定相关的标志,并根据具体的指令与方向标志来递增或递减si/esi/rsi及di/edi/rdi。

(3).如果这条重复指令是REPE/REPNE或REPZ/REPNZ,就根据ZF标志来判断是否应该继续重复。如果不应该就不再重复。

(4).如果第2步(对于另外两种形式来说是第2与第3步)顺利执行,那就将相关的C系列寄存器减1并回到第1步。

7.3 基本字符串指令:5种处理字符串数据的指令,如下表所示:这些字符串指令都会根据方向标志(也就是DF)的取值,给si/esi/rsi及di/edi/rdi加上或减去一定的字节数,使得程序能够按照开发者预想的方向来处理数组。即便不与REP搭配,这些字符串指令也依然会执行递增或递减运算。

MOVS(move string, 移动字符串):与MOV指令类似,但是有两个区别,第一,它的源操作数与目标操作数不需要写在指令中,而是默认由esi及edi等寄存器来表示。第二,所有的汇编器都要求在该指令后面写上一个与元素大小相对应的后缀字母。si/esi/rsi寄存器指向待复制的数据所在的内存位置,di/edi/rdi寄存器指向目标地址。在与REP相搭配用以连续复制多个字符的过程中,为了使下一次复制操作能够找到正确的字符,开发者必须指出每个字符所占据的字节数,从而令MOVS可以根据DF标志适当地递增或递减相应的寄存器。由于字符串指令每次所操作的数据可能是1、2、4、8或16个字节,则字符串指令所添加的后缀依次为B(byte)、W(word)、D(dword)、Q(qword)、O(octa)。

CMPS(compare string, 比较字符串):指令与CMP指令相似,但是它与MOVS一样也不带操作数,而是默认会通过si/esi/rsi及di/edi/rdi来确定有待比较的数据所在的内存地址,而且它也像MOVS那样最好是能够与重复指令结合起来使用。如果不搭配重复指令,而是单独使用这条指令,那么只能比较一个字符而不是整个字符串。把重复指令与MOVS相搭配,可以按序复制字符,但若与CMPS相搭配则是按序访问字符。为了对比两个字符串,CMPS指令必须从一个字符移动到下一个字符,然而它在移动的同时还需要判断:当前字符与另一个字符串里的对应字符是否相同。与CMPS搭配的重复指令是REPE/REPZ或REPNE/REPNZ。以REPE/REPZ为例,只有当两个字符串的对应字符相等时才会继续比较下一个字符(因为只有在这种情况下CMPS指令才会把ZF标志设置成1,从而使重复指令能够继续执行)。CMPS通过减法比较这两个字符,也就是把由si/esi/rsi所表示的字符从由di/edi/rdi所表示的字符值中减去,并根据结果修改相关的处理器标志,尤其是ZF标志。REPE与PEPZ是同一个意思,因为它们执行相同的判断逻辑。REPNE与REPNZ两者之间也是如此。CMPS指令最基础的用法是单独以CMPS+后缀的格式使用。

SCAS(scan string, 扫描字符串):实际上是在执行内置的顺序搜索算法,该算法在搜寻字符串的过程中查找的是单个字符。SCAS的正常流程是对di/edi/rdi所指的字符串做迭代,以便在其中寻找与al/ax/eax/rax里的目标字符相同的字符,如果能找到就提取退出,与它相搭配的是REPNE或REPNZ指令。与CMPS相搭配时,REPE/REPZ指令不会在CMPS失败的时候递减cx/ecx/rcx寄存器。与SCAS相搭配时,REPNE/REPNZ指令总是会递减cx/ecx/rcx寄存器,而不考虑SCAS指令是成功还是失败。SCAS指令最基础的用法是单独以SCAS+后缀的形式使用。

STOS(store string, 存储字符串):很适合用来初始化数组。它会把累加寄存器里的值复制到由di/edi/rdi寄存器所表示的内存位置上。如果有多个位置(例如整个数组)都需要用这个值来初始化,那么可以将其与REP结合起来使用。STOS无法用不同的值来初始化数组中的不同元素,它只能将同一个值复制到数组中的每一个位置上。STOS指令最基础的用法是单独以STOS+后缀的形式使用。

LODS(load string, 加载字符串):与STOS相反,它不是把值从累加寄存器复制到di/edi/rdi所表示的地址上,而是从si/esi/rsi所指的地址上取值并将其复制到累加寄存器中。与STOS不同,这条指令一般不与重复指令相结合,因此那样做会导致累加寄存器频繁遭到覆写。LODS指令最基础的形式是LODS+后缀。

7.4 结构体:是复合的数据类型,与高级语言类似,汇编语言中的结构体也是由用户所定义的数据类型,能够包含多个字段,这些字段的数据类型可以互不相同。数组中的所有元素都必须一样大,而结构体中的每个元素大小则未必相同。没有能把整个结构体所占据的字节数告诉编译器的命令,因此,它需要知道结构体的起始位置及结束位置。知道了这两个位置,NASM就可以据此推断出结构体的总尺寸,这就好比知道了当前位置计数器与数组的起始位置我们就可以用EQU来确定数组长度。

由于结构体不是内置的数据类型,因此必须先把它定义出来,然后才能声明这种结构体的实例。结构体必须定义在code与data段之外,也就是称为absolute段的地方,或者也可以定义在另一份文件中并通过INCLUDE命令将其包含进来。这两种写法可以确保结构体在实例化之前已经具备适当的定义。结构体定义、声明及使用范例如下表所示:有的时候必须通过.ALIGN、.ALIGNB等对齐命令来确保结构体及其成员能够对齐到适当的内存边界处。

8. 浮点运算

8.2 浮点数的表示方式:计算机中的浮点值(floating-point value)是一种用来近似表示实数的数据形式,这种值的小数点前后通常都有一些数位。浮点数由有效数(significand)、基数与指数三部分组成,其中的有效数部分可以容纳固定个数的有效数位,而基数则用来与指数相配合,以便对有效数放大或缩小。所谓浮点是指有效数部分只中的小数点能够左右移动,只要相应地调整基数的指数,就能保持整个值不变。

IEEE表示法:1985年,电气电子工程师协会发布了IEEE 754技术标准,用以规范浮点数的计算。这份标准在2008年更新为IEEE 754-2008标准。下表列出了几种常见的IEEE浮点数格式,其中最常用的是单精度、双精度与扩展双精度格式。四倍精度虽然也定义在IEEE标准中,但是一般的硬件都不支持。

下表演示了把78.375转换成IEEE 754单精度格式的过程:

特殊值:这些值包括带符号的0、无穷,以及NaN(Not-a-Number, 非数)。下表以十六进制形式列出了这些特殊的值:NaN是一种用来表示未定义值或假想值的数据。

次正规数(subnormal number):也叫做非正规数(denormalized number),它们可以表示正规范围以外的值。

舍入:舍入模式如下表所示:用浮点值做运算或者在不同的浮点格式之间转换时,需要考虑舍入方法。大多数系统的默认舍入方法是向最近的值舍入(round to nearest)。

8.3 浮点数的实现:为了支持浮点数,Intel开发了8087及80386辅助处理器用以充当FPU(floating point unit, 浮点运算单元),从而与CPU协同运作。这些FPU实现了名为x87的指令集架构。从80486/487开始,FPU集成到了CPU中。

x87:定义了自己的一套指令、寄存器及浮点数格式以执行浮点运算。下表列出了x87寄存器:

浮点运算的通用寄存器是R0至R7这八个寄存器。系统每执行完一条FPU指令就会把该指令的内存地址放入Last Instruction Pointer中,如果该指令还带有操作数,那么操作数的内存地址会放在Last Data Pointer中,如下表所示。把刚刚执行过的那条指令及其操作数所处的内存地址记录下来,是为了给异常处理程序提供状态信息。

状态寄存器用来保存FPU的当前状态,如下表所示:其中11至13号这三位合称TOP,用来表示栈顶(top-of-stack)。FPU的8个数据寄存器(R0至R7)形成循环栈,其中任何一个寄存器都有可能充当栈顶。如果向R0推入一个值,那么下一个值就会推入R7中。从栈中删除元素时也会出现这样的循环现象,只不过方向与推入元素时相反。为了利用这种特性,开发者并不直接以R0至R7这样的名称来指代寄存器,而是以ST0至ST7等写法来指代它们,哪个R寄存器充当栈顶就把哪个R寄存器叫做ST0.这个R寄存器在整个R系列中的序号保存在FPU状态寄存器的TOP字段中。

状态寄存器中的各个二进制,其含义如下:

(1).FPU busy(15号位):当FPU正在执行某条指令时,该位会设置成1.

(2).Condition codes(8至10号位,以及14号位):用来表示浮点算术的结果。

(3).TOP(11至13号位):用来表示目前充当栈顶的R寄存器是第几个R寄存器。

(4).Error Status(7号位):如果正在处理FPU异常,那么该位的值是1.

(5).Stack Fault(6号位):如果置位,那么意味着FPU栈发生了上溢或下溢(试图把数据加载到本身已经具备有效数值的寄存器中,或者试图从已经不含有效数值的寄存器里获取数据)。

(6).Exceptions(0至5号位):P位表示precision(精度受损),U位表示underflow(下溢),O位表示overflow(上溢),Z位表示zero divide(除以0),D位表示denormalized operand(非正规的操作数),I位表示invalid operation(无效操作)。其中,P、U、O三种异常,有可能发生在执行完某项操作之后,而Z、D、I这三种异常则有可能发生在即将执行某项操作之前。

下表描述了FPU的控制寄存器(Control Register),其中有一些二进制位用来控制无穷、舍入以及精度等几个方面,还有一些二进制位充当异常掩码。每一个掩码位都对应于状态寄存器中相应异常位。执行完FPU初始化(FPU initialization, FINIT)指令之后,所有的掩码位都会默认设置成1,这意味着所有的浮点异常都交由FPU处理。如果开发者把某个掩码位设置为0,那么发生相应的浮点异常时系统就会产生中断,从而把异常交给程序来处理。6、7、13、14及15号位是保留位,或者说暂时没有用到的二进制位。12号位叫做infinity control位,当今的x87处理器已经不使用这一位了。每一个异常掩码位所控制的异常:PM控制precisioon(精度受损),UM控制underflow(下溢),OM控制overflow(上溢),ZM控制zero divide(除以0),DM控制denormalized operand(非正规的操作数),IM控制invalid operation(无效操作)。

x87 FPU接受7种格式的数字:单精度、双精度、扩展双精度、单字整数、双字整数、四字整数,以及压缩的BCD整数(packed BCD integer, 其中BCD的意思是binary coded decimal,也就是用二进制码来表示的十进制数)。

值加载到FPU的时候默认会转换成扩展双精度格式(该格式所占据的二进制位个数为80),这样可以令精确程度最高。如果要把这样的值存入内存,可以沿用扩展双精度格式,也可以转换为更为短小的格式。控制寄存器里的精度控制(PC)字段能够控制格式。扩展双精度与压缩BCD这两种格式的支持情况在各平台及汇编器之间可能有所不同。

x87 FPU采用后缀表示法(postfix notation)来做计算(这和某些计数器是一样的)。比方说,做加法的时候是先把有待相加的两个操作数推入栈中,然后再处理它们之间的加法。

x87的FPU指令以字母F开头。第二个字母如果是B或I,意味着内存操作数应该解读为压缩的BCD格式或整数格式,若不是这两种字母则应解读为浮点数格式。

MASM要求栈寄存器的序号必须用一对圆括号括起来,而NASM则不要求写出这对括号。

x87 FPU中还有两个寄存器也用来保存与FPU有关的信息。其中一个是Tag Register(标记寄存器),它用来表示FPU数据寄存器(也就是R0至R7这8个寄存器)中存放的是何种内容。有了Tag Register,开发者就可以先判断相关寄存器中的内容是否合适,然后再去执行更为复杂的指令或解码任务。FSTENV/FNSTENV或FSAVE/FNASVE指令可以把FPU的Tag Register存储到内存中。软件不能直接修改该寄存器。与某个R寄存器相对应的tag字段如果是00,那么表示该R寄存器中是个有效的非零值。如果是01则表示其内容是0或与0等价的值。如果是10则表示其内容是NaN、inf(无穷)、-inf(负无穷)或非正规数等特殊值。如果是11则表示该R寄存器是空的。FINIT指令或弹出指令会把Tag Register中相应字段标为empty。

另外一个是Opcode Register(操作码寄存器),它用来存放最近执行的那条非控制指令。

MMX:本身并非用于浮点数计算,而是通过FPU寄存器来操作整数。Intel在1996年发布P55C(80503)处理器时引入了这项技术。MMX处理器配有8个可用于整数运算的64位寄存器以及47条指令。这8个64位寄存器分别用MM0至MM7来表示,它们其实是80位FPU数据寄存器R0至R7中有效数那一部分的别名。

MMX技术有两大特性,一个是压缩的整数型数据(packed integer data),另一个是单指令流多数据流(Single Instruction Multiple Data, SIMD)。64位的MMX寄存器可以存放压缩形式的字节、字及双字。

向MMX寄存器中移入数据或是将MMX寄存器中的数据移出,依然要按照普通的方式来做,或者要以四字(也就是64个二进制位)为单位来做,不过,保存在寄存器中的这些压缩数据,却可以在同一时间分别参与各自的算术运算。像这种通过一条指令并行地操作多个数据点的理念称为SIMD模型。

虽然MMX与FPU指令操作的是同一套寄存器,但这两种指令其实可以用在同一个程序中,只不过这样做会产生一些开销。

SSE(Streaming SIMD Extensions, 流式SIMD扩展):构建在MMX技术之上。MMX所处理的数据需要以压缩的整数形式保存在64位寄存器中,而SSE所处理的浮点数据则既可以用压缩的形式保存又可以直接用标量的形式保存,而且是保存在128位寄存器中的。请注意,凡是提到压缩形式的数据,就意味着这些数据能够在程序运行过程中并行地得到处理

SSE指令可以分成四大类:用于操作压缩浮点数及标量浮点数的指令;SIMD整数指令;状态管理指令;缓存控制指令、用来对指令的执行顺序/内存的操作顺序做出安排的指令。

可以在32位模式下使用的128位SSE寄存器是xmm0至xmm7 8个XMM寄存器。在64位模式下还可以再使用8个寄存器,也就是xmm8至xmm15。必须注意,在兼容SSE的处理器中,这些寄存器并不是其它寄存器的别名而是单独的寄存器。XMM寄存器只能存放数据不能用来存放地址。

SSE还有一个32位的控制及状态寄存器叫做MXCSR,它与x87的控制寄存器及状态寄存器类似。MXCSR包含一些与浮点异常及舍入控制有关的标志与掩码,还有各种用来控制SIMD操作的标志。

SSE只支持一种数据类型即单精度的浮点数。SSE2支持更多的数据类型,其中包括双精度浮点数、四字整数、双字整数、单字整数(也称为短整数,short)、字节整数。SSE的标量指令仅适用XMM寄存器中最低的双字或四字部分来执行浮点运算。大多数SSE指令都有两种形式,一种针对标量形式的数据,另一种针对压缩形式的数据。目前的大多数C++编译器都采用标量形式的SSE指令计算浮点数,而不会自动采用压缩形式的SSE指令来并行地执行多项计算。Intel的C++编译器是个例外。

对于MMX、SSE及SSE2技术来说,所有的算术与比较运算都可以在寄存器所压缩的各项数据之间并行地执行。SSE3除了能像早前的技术那样同时给某一对寄存器内的多份数据执行计算之外,还能在不同的寄存器上同时执行各自的运算,并把运算结果汇总到目标寄存器的相应部分中。

AVX(Advanced Vector Extensions, 高级向量扩展):能够向后兼容SSE。出现了三套高级扩展指令,它们分别是:AVX指令集、AVX2指令集以及AVX-512指令集。这些扩展指令集较大地提升了寄存器的容量,并使得开发者能够运用更多的指令来编写程序。

Windows下链接汇编代码及C++代码操作步骤(x64)

(1).创建一个Win32控制台应用程序AssemblyLanguage_Test,并将其调整为x64;

(2).添加几个新文件,AssemblyLanguage_Test.cpp里面主要包含main函数;funset.asm里面包含了一些汇编代码;

(3).通过ml64.exe对funset.asm做汇编,产生目标文件funset.obj:

A.在C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64目录下启动cmd.exe;

B.运行vcvars64.bat或者直接将vcvars64.bat直接拖到cmd中;

C.定位到E:\GitCode\CUDA_Test\demo\AssemblyLanguage_Test\masm,执行:$ ml64.exe /c /Cx funset.asm,将会生成funset.obj,执行结果如下图所示:

(4).将funset.obj添加到工程中;

(5).生成并运行程序。

如果修改了funset.asm,那么必须通过ml64.exe重新生成funset.obj。

Ubuntu下链接汇编代码及C++代码操作步骤(x64)

(1).添加几个新文件,AssemblyLanguage_Test.cpp里面主要包含main函数;funset.asm里面包含了一些汇编代码;

(2).build.sh内容如下:

#! /bin/bash

real_path=$(realpath $0)
echo "real_path: ${real_path}"
dir_name=`dirname "${real_path}"`
echo "dir_name: ${dir_name}"

new_dir_name=${dir_name}/build
if [[ -d ${new_dir_name} ]]; then
	echo "directory already exists: ${new_dir_name}"
else
	echo "directory does not exist: ${new_dir_name}, need to create"
	mkdir -p ${new_dir_name}
fi

rc=$?
if [[ ${rc} != 0 ]]; then
	echo "########## Error: some of thess commands have errors above, please check"
	exit ${rc}
fi

cd ${new_dir_name}
cmake ..
make

cd -

(3).CMakeLists.txt内容如下:

PROJECT(AssemblyLanguage_Test)
CMAKE_MINIMUM_REQUIRED(VERSION 3.0)

# support C++11
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# support C++14, when gcc version > 5.1, use -std=c++14 instead of c++1y
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y")

IF(NOT CMAKE_BUILD_TYPE)
	SET(CMAKE_BUILD_TYPE "Release")
	SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2")
	SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")
ELSE()
	SET(CMAKE_BUILD_TYPE "Debug")
	SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -Wall -O2")
	SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall -O2")
ENDIF()
MESSAGE(STATUS "cmake build type: ${CMAKE_BUILD_TYPE}")

MESSAGE(STATUS "cmake current source dir: ${CMAKE_CURRENT_SOURCE_DIR}")
SET(PATH_TEST_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/./../../demo/AssemblyLanguage_Test)
SET(PATH_TEST_NASM_FILES ${CMAKE_CURRENT_SOURCE_DIR}/./../../demo/AssemblyLanguage_Test/nasm)
MESSAGE(STATUS "path test files: ${PATH_TEST_CPP_FILES} ${PATH_TEST_NASM_FILES}")

# head file search path
INCLUDE_DIRECTORIES(${PATH_TEST_CPP_FILES})

FILE(GLOB TEST_CPP_LIST ${PATH_TEST_CPP_FILES}/*.cpp)
FILE(GLOB TEST_CPP2_LIST ${PATH_TEST_CPP_FILES}/nasm/*.cpp)
MESSAGE(STATUS "test cpp list: ${TEST_CPP_LIST} ${TEST_CPP2_LIST}")

FILE(GLOB_RECURSE TEST_NASM_LIST ${PATH_TEST_NASM_FILES}/*.asm)
MESSAGE(STATUS "test nasm list: ${TEST_NASM_LIST}")

# build nasm
SET(CMAKE_ASM_NASM_FLAGS_DEBUG_INIT "-g")
SET(CMAKE_ASM_NASM_FLAGS_RELWITHDEBINFO_INIT "-g")

IF(NOT DEFINED CMAKE_ASM_NASM_COMPILER AND DEFINED ENV{ASM_NASM})
	SET(CMAKE_ASM_NASM_COMPILER $ENV{ASM_NASM})
ENDIF()

ENABLE_LANGUAGE(ASM_NASM)
MESSAGE(STATUS "CMAKE_ASM_NASM_COMPILER = ${CMAKE_ASM_NASM_COMPILER}")

IF(CMAKE_ASM_NASM_OBJECT_FORMAT MATCHES "elf*")
	SET(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -DELF")
	SET(CMAKE_ASM_NASM_DEBUG_FORMAT "dwarf2")
ENDIF()
  
SET(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -D__x86_64__")

MESSAGE(STATUS "CMAKE_ASM_NASM_OBJECT_FORMAT = ${CMAKE_ASM_NASM_OBJECT_FORMAT}")

IF(NOT CMAKE_ASM_NASM_OBJECT_FORMAT)
	MESSAGE(FATAL_ERROR "could not determine NASM object format")
	return()
ENDIF()

get_filename_component(CMAKE_ASM_NASM_COMPILER_TYPE "${CMAKE_ASM_NASM_COMPILER}" NAME_WE)
MESSAGE(STATUS "CMAKE_ASM_NASM_COMPILER_TYPE = ${CMAKE_ASM_NASM_COMPILER_TYPE}")

IF(NOT WIN32 AND (CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED))
	SET(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -DPIC")
ENDIF()
  
ADD_LIBRARY(simd OBJECT ${TEST_NASM_LIST})
IF(NOT WIN32 AND (CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED))
  set_target_properties(simd PROPERTIES POSITION_INDEPENDENT_CODE 1)
ENDIF()

# build executable program
ADD_EXECUTABLE(AssemblyLanguage_Test ${TEST_CPP_LIST} ${TEST_CPP2_LIST})
TARGET_LINK_LIBRARIES(AssemblyLanguage_Test simd)

(4).执行:$ ./build.sh; ./build/AssemblyLanguage_Test

GitHub:https://github.com/fengbingchun/CUDA_Test

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

汇编程序设计与计算机体系结构软件工程师教程笔记:函数、字符串、浮点运算 的相关文章

  • 推送 64 位 intel osx

    我想将 64 位地址压入堆栈 如下所示 asm pushq 0x1122334455667788 但我得到编译错误 我只能按以下方式推送 asm pushq 0x11223344 有人可以帮助我理解我的错误吗 我是装配新手 所以如果我的问题
  • 汇编编程语言:程序仅当输入为 ESC 时退出,并在退出前要求确认(y/n),否则循环

    我只是汇编语言编程的初学者 我们的第一个任务是让程序仅在输入为 ESC 时退出 退出之前请求确认 y n 否则循环 我知道 ESC 在 ASCII 代码中具有等效值 但我对插入位置或是否需要添加更多内容感到困惑 请帮我 这是程序 model
  • 为什么这个 C++ 包装类没有被内联掉?

    EDIT 我的构建系统出了问题 我还在弄清楚到底是什么 但是gcc产生了奇怪的结果 尽管它是 cpp文件 但是一旦我使用了g 然后它按预期工作 对于我一直遇到麻烦的事情来说 这是一个非常精简的测试用例 其中使用数字包装类 我认为会内联 使我
  • 汇编指令陷阱有什么作用?

    当程序需要时 程序通常会发出软件陷阱 由操作系统提供服务 通用异常处理程序 操作系统确定陷阱的原因并做出响应 适当地 汇编指令 trap 和 BASIC 中的 TRAP 指令一样吗 答案似乎是肯定的 你能接受还是拒绝我的结论 不中断 的代码
  • 遍历内存编辑每个字节

    我正在编写汇编代码 提示用户输入一串小写字符 然后输出包含所有大写字符的相同字符串 我的想法是迭代从特定地址开始的字节 并从每个字节中减去 20H 将小写变为大写 直到到达具有特定值的字节 我对 Assembly 相当缺乏经验 所以我不确定
  • AVX512 掩码寄存器(k1...k7)的 GNU C 内联 asm 输入约束?

    AVX512 为其算术命令引入了 opmask 功能 一个简单的例子 上帝螺栓 org https godbolt org z P7xWD8 include
  • Clang 使用 -nostdlib 生成崩溃代码

    我正在尝试为可执行文件设置自己的运行时环境 但无法使用 clang v3 4 1ubuntu1 目标 x86 64 pc linux gnu 来生成没有段错误的可执行文件 我已将问题简化为以下内容 如果我有一个文件 crt1 c 除了满足
  • 如何让c代码执行hex机器代码?

    我想要一个简单的 C 方法能够在 Linux 64 位机器上运行十六进制字节码 这是我的 C 程序 char code x48 x31 xc0 include
  • 为什么 RISC-V S-B 和 U-J 指令类型以这种方式编码?

    我正在读一本书 计算机组织与设计RISC V版 我遇到了 S B 和 U J 指令类型的编码 我上面提到的那些类型有奇怪的编码立即字段 S B 类型将直接字段分为两部分 这是有道理的 因为所有指令编码都必须相似 但我无法理解为什么立即字段以
  • 在 x86 ASM 中测试零通常哪个更快:“TEST EAX, EAX”与“TEST AL, AL”?

    测试 AL 中的字节是否为零 非零通常哪个更快 TEST EAX EAX TEST AL AL 假设之前有一个 MOVZX EAX BYTE PTR ESP 4 指令加载了一个带有零扩展的字节参数到 EAX 的其余部分 防止了我已经知道的组
  • 为什么在强度降低乘法和循环进位加法之后,这段代码的执行速度会变慢?

    我正在读书阿格纳 雾 https en wikipedia org wiki Agner Fog s 优化手册 https en wikipedia org wiki Agner Fog Optimization 我遇到了这个例子 doub
  • 68HC11计算sin(x)的汇编代码

    68HC11 使用泰勒级数或查找表计算正弦值的汇编代码是什么 显示值只能是整数 查找表如何工作 在这种情况下 如何使用它来实现泰勒级数 http en wikipedia org wiki Taylor series 如果您正在寻找浮点解决
  • 有没有办法使用 i387 fsqrt 指令获得正确的舍入?

    有没有办法使用 i387 fsqrt 指令获得正确的舍入 除了改变精确模式在 x87 控制字中 我知道这是可能的 但这不是一个合理的解决方案 因为它存在令人讨厌的重入型问题 如果 sqrt 操作中断 精度模式将出错 我正在处理的问题如下 x
  • 如何知道寄存器是否是“通用寄存器”?

    我试图了解寄存器必须具备什么标准才能被称为 通用寄存器 我相信通用寄存器是一个可以用于任何用途的寄存器 用于计算 将数据移入 移出等 并且是一个没有特殊用途的寄存器 现在我读到了ESP寄存器是通用寄存器 我猜是ESP寄存器可以用于任何事情
  • 弹出 x86 堆栈以访问函数 arg 时出现分段错误

    我正在尝试链接 x86 程序集和 C 我的C程序 extern int plus 10 int include
  • 如何在 Debian 上编译 DOS 程序?

    在我的汇编语言课程中 我们使用 DPMI 编写 DOS 程序 不幸的是 我无法一直使用 32 位 Windows 机器 我在我使用的几乎每台计算机上都安装了 Debian 虚拟机 我已经安装了 DOSBox 和 DOSEMU 有什么办法可以
  • 阴影空间示例

    EDIT 我接受了下面的答案 并添加了我自己的代码的最终修订版 希望它向人们展示影子空间分配的实际示例 而不是更多的文字 编辑 2 我还设法在 YouTube 视频 所有内容 的注释中找到了一个调用约定 PDF 的链接 其中有一些关于 Li
  • 如何将 x86 GCC 风格的 C 内联汇编转换为 Rust 内联汇编?

    我在 C 中有以下内联汇编 unsigned long long result asm volatile byte 15 byte 49 shlq 32 rdx orq rdx rax a result rdx return result
  • 这个反斜杠在这段汇编代码中起什么作用?

    我不确定这些推线有什么区别 修剪下来来自 Linux 的 x86 entry calling h https github com torvalds linux blob 241e39004581475b2802cd63c111fec43b
  • 如何仅使用单个数组在 JavaScript 中模拟调用堆栈

    我正在看维基百科页面 https en wikipedia org wiki Call stack在调用堆栈上 并尝试理解这个图像 据我所知 哈哈 const memory memory 0 3 top of stack pointer m

随机推荐

  • 主合取/析取范式

    前置知识 简单合取 析取式 合取 析取范式 极小项 当存在n个命题变项做合取时 如果这个简单合取式出现了全部的命题变项或它的否定形式 且恰好只出现一次 则这个式子属于极小项 以n 3 命题变项为p q r为例 他们的极小项如表 主析取范式
  • Ubuntu16升级为18

    https blog csdn net sean 8180 article details 81075659
  • 从mimikatz抓取密码学习攻防

    前不久在使用mimikatz抓取hash的时候遇到了报错 本着追根溯源的原则去查看了mimikatz抓取密码的原理 在学习的过程中发现了mimikatz的每种报错都有不同的原因 本文就从mimikatz的防御角度出发来分析如何防御mimik
  • 【ubuntu】报错su:认证失败

    问题 ubuntu使用su命令时提示认证失败 解决方法 报这个错误的原因是root用户默认锁定的 只要使用passwd命令重新设置下root密码即可 详细步骤 1 命令行输入sudo passwd 2 根据提示修改密码 3 输入su 4 输
  • Altium Designer编辑PCB时,器件跑出可视界面外的解决方法

    很多人在使用AD等进行PCB设计的时候 由于制作封装问题或者是其他操作问题 会遇到在PCB界面下某一个或者几个封装超出软件显示范围 不论如何移动和放大缩小 都无法显示出来 也就没法选中和编辑 下面就讲讲如何解决这个问题 一个封装超出界外 不
  • Feign 使用 @SpringQueryMap 来解决多参数传递问题

    本文目录 1 Feign传递一个bean对象参数 2 Feign传递一个bean对象参数 多个基本类型参数 3 Feign传递多个基本类型参数 4 Feign传递多个bean对象参数 在实际项目开发过程中 我们使用 Feign 实现了服务与
  • css_流光按钮(转载)

    CSS部分 初始化 取消页面的内外边距 padding 0 margin 0 body 弹性布局 让页面元素垂直 水平居中 display flex justify content center align items center 让页面
  • Mock框架应用(三)-Mock Post请求

    不带参数的post请求 description 不含参数的post请求 request uri post method post response text 不含参数的post请求 带参数的post请求 配置Json 并启动moco服务 d
  • python列表index找不到索引_Python list.index在找不到索引时抛出异常

    Why does list index throw an exception instead of using an arbitrary value for example 1 What s the idea behind this To
  • R语言数据清洗与规整-回归模型为例

    数据清洗和规整是进行数据分析的前提条件 数据的清洗和规整通常会花费比进行数据分析更多的时间 正所谓 清洗一小时 分析五秒钟 数据清洗和规整要依据实际数据的特征进行 其包括缺失值和冗余值的处理 数据重归类 字符类型转换等 这里将使用 狗熊会
  • Opencv C++ 基本数据结构 Mat

    Opencv C 基本数据结构 Mat Mat 构造单通道Mat对象 获取单通道Mat的基本信息 以三行两列的矩阵为例 1 获取行数和列数 2 使用成员函数size 获取矩阵的尺寸 3 使用成员函数channels 获取矩阵的通道数 4 使
  • Unity中的GameObjectRecorder类录制动画

    Unity中的GameObjectRecorder类录制动画 记录 GameObjectRecorder 示例代码 解析 记录 首先是 参考及示例视频 Unity制作战神等级的表情动画 游戏 CG Vtuber适用 相关代码在8分16秒之后
  • 进程间通信(IPC)

    概述 进程间通信就是在不同进程之间传播或交换信息 那么不同进程之间存在着什么双方都可以访问的介质呢 进程的用户空间是互相独立的 一般而言是不能互相访问的 唯一的例外是共享内存区 另外 系统空间是 公共场所 各进程均可以访问 所以内核也可以提
  • 用javaScript编写lrc歌词解析器

    如果想要了解如何编写的请继续往下看 如果只需要代码 请点击这里Github lrc歌词文件介绍 来先看一下以下歌词 Heart To Heart lrc ti Heart To Heart ar James Blunt al Heart T
  • Python Socket连接过程详析

    一 套接字 套接字是为特定网络协议 例如TCP IP ICMP IP UDP IP等 套件对上的网络应用程序提供者提供当前可移植标准的对象 它们允许程序接受并进行连接 如发送和接受数据 为了建立通信通道 网络通信的每个端点拥有一个套接字对象
  • TinyDDS编程实践

    背景 传统计算机网络的运行依赖于集中式运营商及服务提供商 Server 它在拓扑结构上的典型特点就是存在一个或数个 中心节点 整个网络的数据传输和处理都集中在少数节点上 中心节点 是维持整个系统运行的关键 也是控制整个系统运行的中枢 这种拓
  • springboot集成alibaba-druid数据连接池及配置

    一 pom文件中引入依赖
  • 【TensorFlow】tf.nn.conv2d卷积实现+filter计算原理

    本来想转载底层实现过程的 结果发现原文中的实验2错误 在这里说明下fliter工作过程 输入层 就是输入的图像 有可能是三通道的有可能是单通道的 filter 深度与输入层的深度 通道数 相同 输出层 输出层的就是filter遍历输入层后的
  • mysql 创建索引 强制_mysql 强制走索引

    查询是数据库技术中最常用的操作 查询操作的过程比较简单 首先从客户端发出查询的SQL语句 数据库服务端在接收到由客户端发来的SQL语句后 执行这条SQL语句 然后将查询到的结果返回给客户端 虽然过程很简单 但不同的查询方式和数据库设置 对查
  • 汇编程序设计与计算机体系结构软件工程师教程笔记:函数、字符串、浮点运算

    汇编程序设计与计算机体系结构 软件工程师教程 这本书是由Brain R Hall和Kevin J Slonka著 由爱飞翔译 中文版是2019年出版的 个人感觉这本书真不错 书中介绍了三种汇编器GAS NASM MASM异同 全部示例代码都