计算所需的最少寻址模式数是多少?
What is the minimum number of addressing modes necessary for computation?
在 x86 汇编程序中,假定您有
- 立即寻址模式分配号码
- 寄存器寻址模式 for registers
- 内存地址的直接寻址模式,
为什么需要索引和基指针寻址模式?
据我所知,每个都可以用循环代替。
此外,间接模式 似乎也不是很有用,因为您可以简单地使用直接模式来引用内存地址。首先访问一个包含指向内存地址的指针的寄存器的目的是什么?
简而言之,哪些寻址方式是真正需要的?
虽然理论上'addressing mode'可以用来指代操作数类型,但由于不涉及地址,所以有点混乱。英特尔手册使用'addressing mode'来指代内存寻址,我将使用这个定义。
在汇编中,操作数可以是:
- 立即数
- 一个寄存器
- 内存中的一个值(这里的操作数是地址)
在x86架构中,"addressing mode"只针对最后一类操作数:内存操作数(地址),指的是可以用来计算地址的方法。寻址模式可以概括为一个单一的可配置寻址模式:
address = REG_base + REG_index*n + offset
REG_base
、REG_index
、n
和 offset
都是可配置的,并且都可以省略(但显然你至少需要一个)。
address = offset
称为立即寻址、直接寻址或绝对寻址。
address = REG_base
称为寄存器间接寻址。
address = REG_base + REG_index
称为基址加索引寻址。
同样,您可以添加偏移量 (offset
) 和比例 (n
)。
严格来说,您只需要一种模式即可完成所有操作:注册间接寻址 (address = REG
)。有了它,如果你需要访问内存,你可以在寄存器中计算你想要的任何地址,并用它来进行访问。
它还可以通过使用内存代替直接寄存器操作数,并通过算术构造值来代替立即操作数。但是,对于实际的指令集,您仍然需要立即操作数来有效地加载地址,如果您不想要仅指针寄存器,则需要寄存器操作数。
寄存器间接之外的其他寻址方式都是为了方便,确实非常方便:
- 如果您只需要访问内存中的固定变量,立即寻址可以节省一个寄存器。
- Base + offset 对于访问对象成员非常有用:您可以将基地址保存在寄存器中并访问具有固定偏移量的单个成员。无需中间计算或注册即可持有会员地址
- 类似地,索引寻址用于访问数组:您只需更改索引寄存器即可访问数组中的任何值。
- 使用标度,您可以访问多字节变量(例如:
int
)数组,无需额外的寄存器或计算。
- 所有内容的组合可用于访问对象中的数组成员,仍然保留基指针以供潜在访问对象中的其他成员。
这些寻址模式不需要从 CPU 进行太多计算:只需要加法和移位。考虑到 x86 可以在每个周期进行乘法运算,这些运算虽然微不足道,但仍然非常方便。
你只需要一个内存寻址模式,这就是许多经典RISC机器在实践中所做的,例如MIPS、SPARC、RISC-V。他们选择 immediate($register)
是因为固定宽度指令中有立即数的空间。 (有些像 PowerPC 有单独的索引寻址指令,否则你必须在寄存器中进行数学计算才能计算出你想要的地址。)
x86 没有寄存器就做不了什么,所以我认为你不能摆脱寄存器“寻址模式”。一些非常不同的体系结构可能不使用寄存器,而只有堆栈或内存、内存指令。 IDK 他们如何实现指针;也许这样的架构可以做到 memory[memory]
(C 数组表示法)。
计算可能不需要立即数。您可以使用多个寄存器构造任何值。从零开始(xor eax, eax
),inc
它得到一个1,左移它到你想要的任何位置,inc
设置低位,左移它,等等 所以最坏情况下需要 2*popcount(N)
条指令才能将 N
存入寄存器。请注意,立即移位计数将不可用,因此重复移位一个的明显方法(shl eax
,是的,有一个单独的移位编码,或者只使用 add eax, eax
) 将仅取决于最高设置位的位置。所以 log2(N) + popcount(N)
对于明显的转变和增加
绝对(所谓的直接)内存寻址不是最有用的寻址模式。我们可以通过使用一系列指令(见上文)构造地址并使用 [register]
来模拟它。如果我们想减少,我们想放弃它。正如 Jester 指出的那样,将绝对寻址作为我们唯一的形式使用起来会非常不方便(或者不可能?)。
索引显然可用于性能,而非必需:您可以使用单独的指令进行移动和添加。
位移也只是为了性能,所以我们可以去掉它们并强制代码手动添加任何位移。有关方法,请参阅紧接的段落。
我相信 x86 仍然可以通过 只是 register
和 [register]
寻址模式进行任意编程。
使用 register
、[register]
和 immediate
,性能应该不会比完整的 x86 差很多,尽管寄存器压力会更大,因为您需要备用寄存器来更频繁地计算地址。当然还需要更多说明。
如果您打算只有一种内存寻址模式,比 [reg]
更有用 的选择是 [reg+disp]
,就像 RISC 使用的那样。这有时可以通过不必计算另一个寄存器中的地址来节省指令(和备用寄存器),并且仍然允许 LEA 复制和添加一个常量,尽管它当前不能移动和添加两个寄存器。
对于 x86 的当前机器代码格式,这需要一个额外字节(或 4 个字节)的机器代码。信号 reg
直接与 [reg]
-间接 (mem) 与 [reg+disp8]
与 [reg+disp32]
在 ModRM 字节中使用 2 位。
如果对内存的隐式访问不算作寻址模式,您当然可以用 lodsd
和 stosd
模拟 [register]
,但您将无法执行原子读-修改-写操作。不过,这感觉像是作弊。
还有堆栈(push/pop
):我不知道堆栈+寄存器机器是否是图灵完备的,但它肯定不是通常意义上的可编程。当然,如果您修改 e/rsp
,您可以再次模拟 [register]
,但操作数大小的选择少于 lodsb/w/d/q
/ stosb/w/d/q
.
如果包括 16 个 ymm 寄存器,x86 有相当多的 space 用于在寄存器中存储内容。虽然我想不出一种在不使用内存或立即操作数(对于 vextractf128
)的情况下在整数寄存器和 ymm 的高 128b 之间移动数据的方法,但实际上你有更多的 16 个 16B 向量寄存器用于存储堆栈以外的本地状态的槽。尽管如此,它的大小仍然有限,这可能意味着 32 位 386 ISA 中的 8 个 GP 寄存器与 64 位 AVX2 ISA 中的所有 integer/mmx/ymm 寄存器与机器是否图灵完备无关 push/pop、寄存器,除 push/pop.
外,不修改堆栈指针
在 x86 汇编程序中,假定您有
- 立即寻址模式分配号码
- 寄存器寻址模式 for registers
- 内存地址的直接寻址模式,
为什么需要索引和基指针寻址模式? 据我所知,每个都可以用循环代替。
此外,间接模式 似乎也不是很有用,因为您可以简单地使用直接模式来引用内存地址。首先访问一个包含指向内存地址的指针的寄存器的目的是什么?
简而言之,哪些寻址方式是真正需要的?
虽然理论上'addressing mode'可以用来指代操作数类型,但由于不涉及地址,所以有点混乱。英特尔手册使用'addressing mode'来指代内存寻址,我将使用这个定义。
在汇编中,操作数可以是:
- 立即数
- 一个寄存器
- 内存中的一个值(这里的操作数是地址)
在x86架构中,"addressing mode"只针对最后一类操作数:内存操作数(地址),指的是可以用来计算地址的方法。寻址模式可以概括为一个单一的可配置寻址模式:
address = REG_base + REG_index*n + offset
REG_base
、REG_index
、n
和 offset
都是可配置的,并且都可以省略(但显然你至少需要一个)。
address = offset
称为立即寻址、直接寻址或绝对寻址。
address = REG_base
称为寄存器间接寻址。
address = REG_base + REG_index
称为基址加索引寻址。
同样,您可以添加偏移量 (offset
) 和比例 (n
)。
严格来说,您只需要一种模式即可完成所有操作:注册间接寻址 (address = REG
)。有了它,如果你需要访问内存,你可以在寄存器中计算你想要的任何地址,并用它来进行访问。
它还可以通过使用内存代替直接寄存器操作数,并通过算术构造值来代替立即操作数。但是,对于实际的指令集,您仍然需要立即操作数来有效地加载地址,如果您不想要仅指针寄存器,则需要寄存器操作数。
寄存器间接之外的其他寻址方式都是为了方便,确实非常方便:
- 如果您只需要访问内存中的固定变量,立即寻址可以节省一个寄存器。
- Base + offset 对于访问对象成员非常有用:您可以将基地址保存在寄存器中并访问具有固定偏移量的单个成员。无需中间计算或注册即可持有会员地址
- 类似地,索引寻址用于访问数组:您只需更改索引寄存器即可访问数组中的任何值。
- 使用标度,您可以访问多字节变量(例如:
int
)数组,无需额外的寄存器或计算。 - 所有内容的组合可用于访问对象中的数组成员,仍然保留基指针以供潜在访问对象中的其他成员。
这些寻址模式不需要从 CPU 进行太多计算:只需要加法和移位。考虑到 x86 可以在每个周期进行乘法运算,这些运算虽然微不足道,但仍然非常方便。
你只需要一个内存寻址模式,这就是许多经典RISC机器在实践中所做的,例如MIPS、SPARC、RISC-V。他们选择 immediate($register)
是因为固定宽度指令中有立即数的空间。 (有些像 PowerPC 有单独的索引寻址指令,否则你必须在寄存器中进行数学计算才能计算出你想要的地址。)
x86 没有寄存器就做不了什么,所以我认为你不能摆脱寄存器“寻址模式”。一些非常不同的体系结构可能不使用寄存器,而只有堆栈或内存、内存指令。 IDK 他们如何实现指针;也许这样的架构可以做到 memory[memory]
(C 数组表示法)。
计算可能不需要立即数。您可以使用多个寄存器构造任何值。从零开始(xor eax, eax
),inc
它得到一个1,左移它到你想要的任何位置,inc
设置低位,左移它,等等 所以最坏情况下需要 2*popcount(N)
条指令才能将 N
存入寄存器。请注意,立即移位计数将不可用,因此重复移位一个的明显方法(shl eax
,是的,有一个单独的移位编码,或者只使用 add eax, eax
) 将仅取决于最高设置位的位置。所以 log2(N) + popcount(N)
对于明显的转变和增加
绝对(所谓的直接)内存寻址不是最有用的寻址模式。我们可以通过使用一系列指令(见上文)构造地址并使用 [register]
来模拟它。如果我们想减少,我们想放弃它。正如 Jester 指出的那样,将绝对寻址作为我们唯一的形式使用起来会非常不方便(或者不可能?)。
索引显然可用于性能,而非必需:您可以使用单独的指令进行移动和添加。
位移也只是为了性能,所以我们可以去掉它们并强制代码手动添加任何位移。有关方法,请参阅紧接的段落。
我相信 x86 仍然可以通过 只是 register
和 [register]
寻址模式进行任意编程。
使用 register
、[register]
和 immediate
,性能应该不会比完整的 x86 差很多,尽管寄存器压力会更大,因为您需要备用寄存器来更频繁地计算地址。当然还需要更多说明。
如果您打算只有一种内存寻址模式,比 [reg]
更有用 的选择是 [reg+disp]
,就像 RISC 使用的那样。这有时可以通过不必计算另一个寄存器中的地址来节省指令(和备用寄存器),并且仍然允许 LEA 复制和添加一个常量,尽管它当前不能移动和添加两个寄存器。
对于 x86 的当前机器代码格式,这需要一个额外字节(或 4 个字节)的机器代码。信号 reg
直接与 [reg]
-间接 (mem) 与 [reg+disp8]
与 [reg+disp32]
在 ModRM 字节中使用 2 位。
如果对内存的隐式访问不算作寻址模式,您当然可以用 lodsd
和 stosd
模拟 [register]
,但您将无法执行原子读-修改-写操作。不过,这感觉像是作弊。
还有堆栈(push/pop
):我不知道堆栈+寄存器机器是否是图灵完备的,但它肯定不是通常意义上的可编程。当然,如果您修改 e/rsp
,您可以再次模拟 [register]
,但操作数大小的选择少于 lodsb/w/d/q
/ stosb/w/d/q
.
x86 有相当多的 space 用于在寄存器中存储内容。虽然我想不出一种在不使用内存或立即操作数(对于 vextractf128
)的情况下在整数寄存器和 ymm 的高 128b 之间移动数据的方法,但实际上你有更多的 16 个 16B 向量寄存器用于存储堆栈以外的本地状态的槽。尽管如此,它的大小仍然有限,这可能意味着 32 位 386 ISA 中的 8 个 GP 寄存器与 64 位 AVX2 ISA 中的所有 integer/mmx/ymm 寄存器与机器是否图灵完备无关 push/pop、寄存器,除 push/pop.