【问题标题】:Assembler - indirect memory addressing汇编器 - 间接内存寻址
【发布时间】:2017-03-04 16:25:21
【问题描述】:

我是 Assembly 的新手,之前学过一些 Java/C.. 所以这就像在飞行后学习爬行。

我目前正在学习“教程点”,因为它看起来对初学者很友好。

我遇到过这段代码:

MY_TABLE TIMES 10 DW 0  ; Allocates 10 words (2 bytes) each initialized to 0
MOV EBX, [MY_TABLE]     ; Effective Address of MY_TABLE in EBX
MOV [EBX], 110          ; MY_TABLE[0] = 110
ADD EBX, 2              ; EBX = EBX +2
MOV [EBX], 123          ; MY_TABLE[1] = 123
  1. 将 MY_TABLE 的 有效地址 分配给 EBX ,有点像 指针 指向表的第一个值? (我知道它无法与 C 相提并论......但我必须以某种方式理解它:))

  2. 它说 ADD EBX , 2 (这意味着 inc EBX 寄存器按 2 ,但是当它被引用到下一行时,它不应该引用到 [2] 地方? 再次.. 将其视为 C 的数组。

希望这个问题是当场的。 我要提前感谢大家。

【问题讨论】:

    标签: assembly x86 nasm addressing-mode


    【解决方案1】:

    MOV EBX, [MY_TABLE] 将从地址 MY_TABLE 的内存中读取 4 个字节(即“数组”的前两个元素)。

    根据评论,我猜作者打算使用LEA EBX, [MY_TABLE]

    LEA 是“加载有效地址”的缩写,它基本上是指令MOV 的前半部分,带有内存引用变体。它将计算最终地址,这样MOV 将从那里加载数据,但不是联系内存芯片并加载数据,而是将地址本身加载到目标寄存器中。同样可以通过MOV ebx,MY_TABLE 在 NASM 中实现,或者通过MOV ebx,OFFSET MY_TABLE 在 MASM/TASM 中实现。

    所以是的,它就像指向表中第一个值的指针,但要充分理解 Assembly 的原始性,您应该记住,它就像指向表中第一个 BYTE 的指针,因为 Assembly 中的指针没有任何类型,在具有平面内存模型(Win32、linux32)的 32b 保护模式下,地址为uint32_t,并且内存可以按字节寻址,因此每个这样的数字都指向内存中的某个位置(物理 RAM 芯片/设备 I/O 映射的位置,或者进入空的无效区域)。这将简单的平面内存 32b 模式限制为 4GiB 的内存空间(x86 允许在 32b 模式下使用更复杂的内存映射方案,这允许寻址超过 4GiB 的内存地址空间,但当然不能使用单个 32b 寄存器地址)。

    2) 因为该指针指向 BYTES,并且您的表来自 WORDS,所以每个字的大小为 2 个字节。表的第一个元素位于偏移量0,但也占用了偏移量1 的下一个字节。第二个值的第一个字节(C 中的 table[1])位于偏移量2。因此add ebx,2 用于获取第二个值的地址。 add ebx,1 会让你指向第一个 my_table[0] 值的中间。

    尽量不要像 C 对应物那样引用这些东西,数组本身是低级的,所以它经常匹配,但任何更高级别的 C 构造通常只会使它更加混乱。 C 数组已经对您隐藏了指针算法,使用元素类型为您计算正确的地址。在装配中不会发生。

    32b x86 如何处理类似 C 的值 my_table[1] 的其他变体:

    lea  ebx,[my_table]
    mov  esi,1
    mov  ax,[ebx + esi*2]  ; by index register scaled by 2
    
    lea  ebx,[my_table]
    mov  esi,2
    mov  ax,[ebx + esi]    ; by offset
    
    lea  ebx,[my_table]
    mov  ax,[ebx + 2]      ; by immediate offset
    
    mov  ax,[my_table + 2] ; by absolute address
    

    编辑:现在我终于注意到了你对 ebx 的实际操作。我认为MOV [EBX], 110“草率”的编程风格,因为从mov 这样的两个论点都不清楚数据大小是多少。

    如果你使用mov [ebx],axax 的大小将此类操作的数据宽度定义为 16 位,但110 可以是 8、16 或 32 位立即数(在 64b 模式下甚至可以是 64b)。因此,在这种情况下(内存引用与立即)正确/好的样式是显式指定参数大小,例如:

    mov [ebx], word 110   ; C-like: ((short *)my_table)[some_index] = (short)110;
    

    出于现代 x86 的性能原因,最好避免使用寄存器的 16 位部分,因此当您处理短数组或字节数组时,最好通过将它们扩展为完整的 32 位值来加载它们:

    movzx eax,word [ebx] ; to extend word value [ebx] with zero bits
    movsx ecx,byte [esi] ; to sign-extend value [ebx]
        ; the top most (sign) bit of original value is copied up to fill upper ecx
    

    然后您可以使用寄存器的 32b 变体(上例中的eaxecx)进行所有计算,并仅在计算结束时使用部分axcl 来存储正确的结果(当然要确保计算本身不会因为使用 32 位寄存器/值而产生错误结果)。

    【讨论】:

    • 最好使用movzx/movsx word ptr eax,[ebx+...] 尽量不要使用部分寄存器。
    • @Johan 为什么? AFAIK 如果您只使用ax,这不是问题。仅当您在部分写入后突然开始使用 eax 时才会支付性能损失。
    • @Johan 根据这个agner.org/optimize/microarchitecture.pdf 我看不出有什么原因,你有关于哪个架构发生的一些数据吗?实际上movzx 在 PPro 之前使用速度较慢,而在 AMD CPU 上又慢了十年左右。
    • 您可以在指令表中看到差异:agner.org/optimize/instruction_tables.pdf 在 Skylake 等较新的架构上,16 位移动有 2 个微指令,而不是 1,而 32+位移动可能会被消除,16 位移动不能。在 Ivy Bridge 上,CPU 可以并行执行 3 个movzx,但只能执行 2 个mov r16,[mem]。可以肯定的是小的差异,但无论如何总是使用 movzx/movsx 变体是一个好习惯(不是因为这些微小的差异,而是因为稍后使用完整寄存器时的(错误)依赖性。
    • @Johan 谢谢你的信息。我认为对于教程来说,显示简单的 16 位代码更容易,无需解释为什么 movzx + 使用 eax 进行计算,然后为什么最后只将 ax 存储回内存中。关于性能......您可能会受到内存吞吐量的限制,当您必须处理大块内存时要使用这么多的空闲周期,以至于 1 uop 的差异可能根本无法测量。如果您对缓存中热的小数据进行操作,那么很可能不需要 16b 打包来节省内存。但是使用movzx 听起来不太容易出错,很高兴知道它现在更快了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-05
    • 2019-06-11
    • 1970-01-01
    相关资源
    最近更新 更多