x86_64寄存器rax/eax/ax/al覆盖全局寄存器内容

95

随着广告宣传的普及,现代的x86_64处理器拥有64位寄存器,可以以向后兼容的方式用作32位寄存器、16位寄存器甚至8位寄存器,例如:

0x1122334455667788
  ================ rax (64 bits)
          ======== eax (32 bits)
              ====  ax (16 bits)
              ==    ah (8 bits)
                ==  al (8 bits)

这样的方案可以被字面理解,即人们总是可以使用指定名称来读取或写入寄存器的一部分,并且这是高度逻辑的。事实上,对于32位以下的所有内容都是如此。

mov  eax, 0x11112222 ; eax = 0x11112222
mov  ax, 0x3333      ; eax = 0x11113333 (works, only low 16 bits changed)
mov  al, 0x44        ; eax = 0x11113344 (works, only low 8 bits changed)
mov  ah, 0x55        ; eax = 0x11115544 (works, only high 8 bits changed)
xor  ah, ah          ; eax = 0x11110044 (works, only high 8 bits cleared)
mov  eax, 0x11112222 ; eax = 0x11112222
xor  al, al          ; eax = 0x11112200 (works, only low 8 bits cleared)
mov  eax, 0x11112222 ; eax = 0x11112222
xor  ax, ax          ; eax = 0x11110000 (works, only low 16 bits cleared)

然而,一旦涉及到64位的东西,事情似乎变得相当尴尬:

mov  rax, 0x1111222233334444 ;           rax = 0x1111222233334444
mov  eax, 0x55556666         ; actual:   rax = 0x0000000055556666
                             ; expected: rax = 0x1111222255556666
                             ; upper 32 bits seem to be lost!
mov  rax, 0x1111222233334444 ;           rax = 0x1111222233334444
mov  ax, 0x7777              ;           rax = 0x1111222233337777 (works!)
mov  rax, 0x1111222233334444 ;           rax = 0x1111222233334444
xor  eax, eax                ; actual:   rax = 0x0000000000000000
                             ; expected: rax = 0x1111222200000000
                             ; again, it wiped whole register

对我来说,这种行为似乎非常荒谬和不合逻辑。看起来似乎通过任何手段将任何内容写入eax寄存器都会导致rax寄存器高32位被擦除。

所以,我有两个问题:

  1. 我相信这种奇怪的行为必须在某处有文档记录,但是我似乎找不到详细的解释(关于64位寄存器的高32位如何被清除的具体说明)。我是否正确,写入eax总是会清除rax,还是说情况更加复杂?它适用于所有64位寄存器吗,还是有一些例外?

    一个相关问题提到了同样的行为,但是,遗憾的是,没有确切的文档参考。

    换句话说,我想要一个指定此行为的文档链接。

  2. 只有我觉得整件事情看起来非常奇怪和不合逻辑吗(即eax-ax-ah-al,rax-ax-ah-al具有一种行为,而rax-eax具有另一种行为)?也许我在这里错过了某种至关重要的点,以至于不得不实现它?

    对“为什么”做出解释将会非常感激。


5
好的,以下是翻译的内容:相关链接:https://dev59.com/uWgu5IYBdhLWcg3wpYjI请注意,我会尽力使翻译更加通俗易懂,但不会改变原本的意思。 - harold
2
这是一个非常好的问题。我刚刚投了赞成票。用户GreyCat,请再看一下第二个代码块,并特别注意第五行。我认为你的评论可能是错误的。 - User.1
1
我认为它应该是:; eax = 0x11110044 - User.1
8
32位操作的结果会隐式地被扩展成64位值。这与16位和8位操作不同,后者不会影响寄存器的高位部分。 - phuclv
1
一些“类似”的问题:https://dev59.com/UGw15IYBdhLWcg3wYKqo?lq=1 https://dev59.com/jWIk5IYBdhLWcg3wqPoi?rq=1 https://dev59.com/pGct5IYBdhLWcg3wqfP9 - phuclv
显示剩余2条评论
1个回答

94

在英特尔/AMD处理器手册中记录的处理器模型是现代核心真实执行引擎的一个相当不完美的模型。其中,处理器寄存器的概念与现实不符,不存在EAX或RAX寄存器。

指令解码器的一个主要任务是将传统的x86/x64指令转换为类似RISC处理器的微操作指令,这些指令小巧易于并发执行,并能利用多个执行子单元。允许最多6个指令同时执行。

为使其正常高效地工作,允许许多指令并发执行,虚拟化了处理器寄存器的概念。指令解码器从一个大的寄存器库中分配一个寄存器。当指令被"退役"时,动态分配寄存器的值被写回到当前保存EAX等寄存器值的寄存器中。

为了使这一过程顺畅高效,允许许多指令并发执行,这些操作之间不能有依赖性。而最糟糕的是,寄存器值依赖于其他指令。EFLAGS寄存器就是臭名昭著的例子,许多指令修改它。

同样的问题也出现在您"喜欢"的方式中。这是一个大问题,需要在指令退役时合并两个寄存器值。创建了一个数据依赖关系,这将阻塞核心。通过强制将高32位设置为0,该依赖性立即消失,不再需要合并,执行速度呼啸而过。


11
我希望你能将这个答案与重复问题的“根”合并。 这是一个非常好的观点,有助于解释设计选择。 - MicroVirus
3
具有物理寄存器文件的微体系结构(例如Intel SnB系列和AMD)会比退役更早向物理寄存器写入结果。我认为你答案中的这部分可能只适用于Intel的P6系列(ppro至nehalem),其通过对物理寄存器的引用而不是值在ROB内保持临时结果。 (P6在读取太多未最近写入的寄存器时会出现寄存器读取停顿。SnB完全消除了这种情况。) - Peter Cordes
4
关于EFLAGS,确实大多数指令会修改它,但是只有少数指令会读取它。您已经提到了寄存器重命名,这使得对EFLAGS的写-after-write依赖不成问题。当然,x86架构并非总是那么简单:某些指令会保持某些标志位不变,因此CPU必须将EFLAGS的不同部分分别重命名。P4架构没有这样做,导致inc指令依赖于最后一个修改标志位的指令。即使P4已经不再使用,一些优化指南仍建议避免使用inc指令。 - Peter Cordes
3
同时,寄存器重命名发生在指令发射时,当微操作(uops)离开前端有序队列进入乱序核心时。这是解码几个阶段之后的事情。更重要的是,在具有微操作缓存和/或循环缓冲区的CPU上,同一条指令可以在解码一次后被多次发射/重命名。你在回答中的简化是对现实的有用近似,因此你能够讲述零扩展如何打破错误依赖关系,而不必包含Agner Fog的微架构PDF 的所有内容 :P - Peter Cordes
我真的不太清楚这些与17年前做出的架构决策有何关联。请发表您自己的答案。 - Hans Passant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接