I am following the book "Beginning x64 Assembly Programming", in Linux 64 system. I am using NASM and gcc.
In the chapter about floating point operations the book specifies the below code 对于 adding 2 float numbers. In the book, and other online sources, I have read that register RAX specifies the number of XMM registers to be used, according to calling conventions.
The code in the book goes as follows:

extern printf
section .data
num1        dq  9.0
num2        dq  73.0
fmt     db  "The numbers are %f and %f",10,0
f_sum       db  "%f + %f = %f",10,0

section .text
global main
main:
    push rbp
    mov rbp, rsp
printn:
    movsd xmm0, [num1]
    movsd xmm1, [num2]
    mov rdi, fmt
    mov rax, 2      ;对于 printf rax specifies amount of xmm registers
    call printf

sum:
    movsd xmm2, [num1]
    addsd xmm2, [num2]
printsum:
    movsd xmm0, [num1]
    movsd xmm1, [num2]
    mov rdi, f_sum
    mov rax, 3
    call printf

That works as expected.
Then, be对于e the last printf call, I tried changing

mov rax, 3

对于

mov rax, 1

然后我重新组装并运行程序.

我期待着一些不同的无意义输出,但我很惊讶输出完全相同.printf正确输出3个浮点值:

The numbers are 9.000000 and 73.000000
9.000000 + 73.000000 = 82.000000

I suppose there is some kind of override when printf is expecting the use of several XMM registers, and as long as RAX is not 0, it will use consecutive XMM registers. I have searched 对于 an explanation in calling conventions and NASM manual, but didn't find one.

这是为什么?

推荐答案

x86-64 SysV ABI的strict rules allow实现只保存指定的XMM reg的确切数量,但当前的实现只判断零/非零,因为这是有效的,尤其是对于AL=0的常见情况.

如果在AL1中传递一个低于XMM寄存器arg实际数的数字,或者传递一个高于8的数字,那么就违反了ABI,只有这个实现细节才能阻止代码中断.(即it "happens to work", but is not guaranteed by any standard or documentation,并且不能移植到其他一些真正的实现中,比如使用GCC4.5或更早版本构建的旧GNU/Linux发行版.)

This Q&A显示了glibc printf的当前版本,它只判断AL!=0,而glibc的旧版本将跳转目标计算为movaps个存储序列.(Q&;A是关于AL>8时的代码中断,使计算出的跳转到不应该跳转的地方.)

Why does eax contain the number of vector parameters?引用了ABI文档,并显示了ICC代码gen,它使用与旧GCC相同的指令进行计算跳转.


Glibc's 100 implementation is compiled from C source, normally by GCC.当现代GCC编译一个变量函数(如printf)时,它使asm只判断零与非零AL,如果非零,则将所有8个通过XMM寄存器的arg转储到堆栈上的一个array.

GCC4.5和更早的版本实际上是did,使用AL中的数字进行计算,跳转到movaps个存储序列中,以实际保存尽可能多的XMM REG.

Nate的简单例子来自GCC4对Godbolt的 comments .5和GCC11显示了与旧/新glibc(由GCC构建)的拆解相关答案相同的差异,这并不令人惊讶.这个函数only使用va_arg(v, double);,从不使用整数类型,所以它不会转储传入的RDI...R9在任何地方,不同于printf.它是一个叶子函数,因此可以使用红色区域(RSP下128字节).

# GCC4.5.3 -O3 -fPIC    to compile like glibc would
add_them:
        movzx   eax, al
        sub     rsp, 48                  # reserve stack space, needed either way
        lea     rdx, 0[0+rax*4]          # each movaps is 4 bytes long
        lea     rax, .L2[rip]            # code pointer to after the last movaps
        lea     rsi, -136[rsp]             # used later by va_arg.  test/jz version does the same, but after the movaps stores
        sub     rax, rdx
        lea     rdx, 39[rsp]               # used later by va_arg, test/jz version also does an LEA like this
        jmp     rax                      # AL=0 case jumps to L2
        movaps  XMMWORD PTR -15[rdx], xmm7     # using RDX as a base makes each movaps 4 bytes long, 与. 5 with RSP
        movaps  XMMWORD PTR -31[rdx], xmm6
        movaps  XMMWORD PTR -47[rdx], xmm5
        movaps  XMMWORD PTR -63[rdx], xmm4
        movaps  XMMWORD PTR -79[rdx], xmm3
        movaps  XMMWORD PTR -95[rdx], xmm2
        movaps  XMMWORD PTR -111[rdx], xmm1
        movaps  XMMWORD PTR -127[rdx], xmm0   # xmm0 last, will be ready for store-forwading last
.L2:
        lea     rax, 56[rsp]       # first stack arg (if any), I think
     ## rest of the function

与.

# GCC11.2 -O3 -fPIC
add_them:
        sub     rsp, 48
        test    al, al
        je      .L15                          # only one test&branch macro-fused uop
        movaps  XMMWORD PTR -88[rsp], xmm0    # xmm0 first
        movaps  XMMWORD PTR -72[rsp], xmm1
        movaps  XMMWORD PTR -56[rsp], xmm2
        movaps  XMMWORD PTR -40[rsp], xmm3
        movaps  XMMWORD PTR -24[rsp], xmm4
        movaps  XMMWORD PTR -8[rsp], xmm5
        movaps  XMMWORD PTR 8[rsp], xmm6
        movaps  XMMWORD PTR 24[rsp], xmm7
.L15:
        lea     rax, [rsp+56]       # first stack arg (if any), I think
        lea     rsi, -136[rsp]      # used by va_arg.  done after the movaps stores instead of before.
...
        lea     rdx, 56[rsp]        # used by va_arg.  With a different offset than older GCC, but used somewhat similarly.  Redundant with the LEA into RAX; silly compiler.

GCC可能改变了策略,因为计算的跳转需要更多的静态代码大小(I-cache内存),而且测试/jz比间接跳转更容易预测.更重要的是,在普通AL=0(无XMM)情况下执行的UOP更少2.即使在AL=1的最坏情况下(7个movaps家店铺死亡,但没有计算分支目标的工作),也没有更多.


相关问题;作为:

半相关,而我们谈论的是违反公约:


Footnote 1: AL, not RAX, is what matters

x86-64 System V ABI doc规定,变量函数必须只查看AL中的REG数;RAX的高7字节被允许存放垃圾.mov eax, 3是设置AL avoiding possible false dependencies from writing a partial register的有效方法,尽管它的机器代码大小(5字节)比mov al,3(2字节)大.clang通常使用mov al, 3.

ABI文件中的要点,更多内容请参见Why does eax contain the number of vector parameters?:

序言应该使用%al,以避免不必要地保存XMM寄存器.这对于仅限整数的程序尤其重要,以防止XMM单元的初始化.

(最后一点是过时的:XMM reg广泛用于memcpy/memset,并内联到zero init小型数组/ struct .如此之多,以至于Linux在上下文switch 上使用"渴望"FPU保存/恢复,而不是在第一次使用XMM reg时出现故障的"懒惰".)

%al的内容不需要与寄存器的数量完全匹配,但必须是所用向量寄存器数量的上限,并且在0到8(含)的范围内.

本ABI对AL<;=8允许计算跳转实现忽略边界判断.(同样,Does the C++ standard allow for an uninitialized bool to crash a program?是的,可以假设ABI违规不会发生,例如,通过编写在这种情况下会崩溃的代码.)


Footnote 2: efficiency of the two strategies

较小的静态代码大小(I-cache内存)总是一件好事,而AL=0战略对它有利.

最重要的是,在AL==0的情况下执行的指令总数更少.printf不是唯一的变量函数;sscanf并不罕见,而且它从不使用FP args(仅指针).如果一个编译器可以看到一个函数从不使用带FP参数的va_arg,它会完全忽略保存,这使得这一点没有意义,但是scanf/printf函数通常被实现为vfscanf/vfprintf调用的包装器,因此编译器doesn't看到,它看到一个va_list被传递给另一个函数,因此它必须保存所有内容.(我认为人们编写自己的可变函数是相当罕见的,因此在许多程序中,对可变函数的唯一调用将是对库函数的调用.)

故障执行官可以在死气沉沉的store 里细嚼慢咽,这对艾尔来说还不错<;8但非零 case ,多亏了广泛的管道和存储缓冲区,开始了与这些存储并行的实际工作.

计算和执行间接跳转总共需要5条指令,不包括lea rsi, -136[rsp]lea rdx, 39[rsp]条指令.test/jz策略也会在movaps存储之后执行这些或类似操作,作为va_arg代码的设置,该代码必须确定何时到达寄存器保存区域的末尾,并切换到查看堆栈参数.

我也不算sub rsp, 48;无论哪种方式,这都是必要的,除非您也将XMM save area size设置为变量,或者只保存每个XMM reg的下半部分,以便8x 8 B=64字节可以放入红色区域.理论上,可变函数可以在XMM reg中使用16字节__m128d arg,因此GCC使用movaps而不是movlps.(我不确定glibc printf是否有需要一次转换的转换).在实际printf这样的非叶函数中,您总是需要保留更多空间,而不是使用红色区域.(这是计算跳转版本中lea rdx, 39[rsp]的一个原因:每movaps需要正好是4个字节,因此编译器生成代码的方法必须确保它们的偏移量在[reg+disp8]寻址模式的[-128,+127]范围内,而不是0,除非GCC将使用特殊的asm语法强制在那里执行更长的指令.

几乎所有x86-64 CPU都以单个微熔合uop的形式运行16字节存储(只有老旧的AMD K8和Bobcat将其拆分为8字节的两部分;参见https://agner.org/optimize/),而且我们通常都会触及128字节区域下方的堆栈空间.(此外,计算出的跳转策略本身存储在底部,因此它不会避免触及缓存线.)

因此,对于具有一个XMM arg的函数,计算跳转版本总共需要6条单uop指令(5条整数ALU/跳转,1条movaps)才能保存XMM arg.

test/jz版本总共需要9个UOP(10条指令,但自Nehalem以来,在Intel上以64位模式进行test/jz宏融合,自推土机IIRC以来,在AMD上以64位模式进行融合).1个宏熔合测试和分支,8个movaps存储.

And that's the best case for the computed-jump version: with more xmm args, it still runs 5 instructions to compute the jump target, but has to run more movaps instructions. The test/jz version is always 9 uops. So the break-even point for dynamic uop count (actually executed, 与. sitting there in memory taking up I-cache footprint) is 4 XMM args which is probably rare, but it has other advantages. Especially in the AL == 0 case where it's 5 与. 1.

test/jz分支对于任何数量的XMM arg总是go 同一个地方,除了零,这使得它变成easier to predict than an indirect branch,这对于printf("%f %f\n", ...)"%f\n"是不同的.

计算跳转版本中的5条指令中有3条(不包括jmp)与传入的AL形成依赖链,这使得需要更多的周期才能检测到预测失误(即使该链可能在调用之前以mov eax, 1开头).但是dump everything策略中的"额外"指令只是一些XMM1的死存储器..7永远不会被重新加载,也不是任何依赖链的一部分.只要store 缓冲区和ROB/RS能够吸收它们,无序执行者就可以在空闲时处理它们.

(公平地说,它们会将存储数据和存储地址执行单元绑定一段时间,这意味着以后的存储也不会很快为存储转发做好准备.在存储地址UOP与加载在同一执行单元上运行的CPU上,以后的加载可能会被占用这些执行单元的存储UOP延迟.幸运的是,现代CPU至少有2个加载执行单元,而从Haswell到Skylake的ntel可以在3个端口中的任意一个上运行存储地址UOP,使用类似这样的简单寻址模式.Ice Lake有2个装载/2个存储端口,没有重叠.)

计算的跳转版本最后保存了XMM0,这可能是第一个重新加载的arg.(大多数变量函数按顺序遍历其参数).如果有多个XMM参数,则计算出的跳转方式在几个周期后才能从该存储向前存储.但对于AL=1的情况,这是唯一的XMM存储,没有其他工作占用加载/存储地址执行单元,少量arg可能更常见.

与较小的代码占用和针对AL==0情况执行的指令更少的优势相比,这些原因中的大多数实际上都很小.(对我们中的一些人来说)思考现代简单方法的上/下两面是很有趣的,它表明即使在最糟糕的情况下,这也不是问题.

Linux相关问答推荐

是否可以在Bash正则表达式中排除?

列出Linux上特定目录和子目录名称的bash命令

Linux 的 __fastfail 替代方案?

为什么在已连接的设备上调用 btmgmt conn-info 返回:状态 0x02(未连接)

为什么库中不调用全局变量的构造函数?

linux shell获取多文件交集

如何恢复已停止的进程?

查找更高版本的文件

使用具有特定值的字段对文件进行排序

在没有root访问权限的情况下安装zsh?

Java 8 上的 SQL Server JDBC 错误:驱动程序无法使用安全套接字层 (SSL) 加密建立与 SQL Server 的安全连接

判断条件是否为假

Linux 应用程序分析

diff 命令仅获取不同行的数量

如何将文件从 Vagrant 机器复制到 localhost

低功耗蓝牙:在 linux 中监听通知/指示

如何在python中找到文件或目录的所有者

grep 递归查找 Linux 上的特定文件类型

在Linux中使用空格设置环境变量

如何拖尾除第一行以外的所有行