首先,如果这个问题不合适,因为我没有提供任何代码或不自行思考,我道歉,我会删除这个问题。
对于赋值,我们需要创建一个节点数组来模拟链表。每个节点都有一个整数值和一个指向列表中下一个节点的指针。这是我的.DATA
部分
.DATA
linked_list DWORD 5 DUP (?) ;We are allowed to assume the linked list will have 5 items
linked_node STRUCT
value BYTE ?
next BYTE ?
linked_node ENDS
我不确定我是否正确定义了STRUCT
,因为我不确定next
的类型应该是什么。另外,我很困惑如何解决这个问题。要将节点插入linked_list
我应该能够写mov [esi+TYPE linked_list*ecx]
,对吗?当然,我每次都需要inc ecx
。我感到困惑的是如何做mov linked_node.next, "pointer to next node"
是否有某种运算符允许我将指针设置为数组中的下一个索引等于linked_node.next
?或者我错误地想到了这个?任何帮助将不胜感激!
答案 0 :(得分:1)
根据您 熟悉的语言考虑您的设计。最好是C,因为C中的指针和值是直接映射到asm的概念。
假设您希望通过存储指向head元素的指针来跟踪链接列表。
#include <stdint.h> // for int8_t
struct node {
int8_t next; // array index. More commonly, you'd use struct node *next;
// negative values for .next are a sentinel, like a NULL pointer, marking the end of the list
int8_t val;
};
struct node storage[5]; // .next field indexes into this array
uint8_t free_position = 0; // when you need a new node, take index = free_position++;
int8_t head = -1; // start with an empty list
有一些技巧可以减少极端情况,比如让列表头部成为一个完整的节点,而不仅仅是一个引用(指针或索引)。您可以将其视为第一个元素,而不必在任何地方检查空列表情况。
无论如何,给定节点引用int8_t p
(其中p是指向列表节点的指针的标准变量名,在链表代码中),下一个节点是storage[p.next]
。下一个节点val
为storage[p.next].val
。
让我们看看asm中的内容。 NASM manual讨论了它的宏系统如何帮助你使用全局结构使代码更具可读性,但我没有为此做过任何宏的事情。您可以为NEXT
和VAL
或其他内容定义宏,其中包含0和1,因此您可以说[storage + rdx*2 + NEXT]
。甚至是一个带参数的宏,所以你可以说[NEXT(rdx*2)]
。如果你不小心,你最终会得到 more 令人困惑的代码。
section .bss
storage: resw 5 ;; reserve 5 words of zero-initialized space
free_position: db 0 ;; uint8_t free_position = 0;
section .data
head: db -1 ;; int8_t head = -1;
section .text
; p is stored in rdx. It's an integer index into storage
; We'll access storage directly, without loading it into a register.
; (normally you'd have it in a reg, since it would be space you got from malloc/realloc)
; lea rsi, [rel storage] ;; If you want RIP-relative addressing.
;; There is no [RIP+offset + scale*index] addressing mode, because global arrays are for tiny / toy programs.
test edx, edx
js .err_empty_list ;; check for p=empty list (sign-bit means negative)
movsx eax, byte [storage + 2*rdx] ;; load p.next into eax, with sign-extension
test eax, eax
js .err_empty_list ;; check that there is a next element
movsx eax, byte [storage + 2*rax + 1] ;; load storage[p.next].val, sign extended into eax
;; The final +1 in the effective address is because the val byte is 2nd.
;; you could have used a 3rd register if you wanted to keep p.next around for future use
ret ;; or not, if this is just the middle of some larger function
.err_empty_list: ; .symbol is a local symbol, doesn't have to be unique for the whole file
ud2 ; TODO: report an error instead of running an invalid insns
请注意,我们通过符号扩展到32位注册表而不是完整的64位rax来减少指令编码。如果值为负,我们不会将rax
用作地址的一部分。我们只是使用movsx
作为清除寄存器其余部分的方法,因为mov al, [storage + 2*rdx]
会将rax
的高位56位留下旧内容。
另一种方法是movzx eax, byte [...] / test al, al
,因为8位test
的编码和执行速度与32位test
指令一样快。此外,作为负载的movzx
在AMD Bulldozer系列CPU上的延迟比movsx
低一个周期(尽管它们仍然采用整数执行单元,与英特尔不同,movsx/zx
完全处理通过加载端口)。
无论哪种方式,movsx
或movzx
都是加载8位数据的好方法,因为您可以避免在写入部分注册表和/或错误依赖项后读取完整注册表的问题(在reg的高位的先前内容上,即使你知道你已将其归零,CPU硬件仍然必须跟踪它)。除非您知道自己没有针对英特尔前Haswell进行优化,否则您不必担心部分寄存器写入。 Haswell执行双簿记或者某些东西以避免额外的uops在阅读时将部分值与旧的完整值合并。 AMD CPU,P4和Silvermont不会与full-reg分开跟踪部分注册表,所以你只需要担心错误依赖。
另请注意,您可以加载next
和val
打包在一起,例如
.search_loop:
movzx eax, word [storage + rdx*2] ; next in al, val in ah
test ah, ah
jz .found_a_zero_val
movzx edx, al ; use .next for the next iteration
test al, al
jns .search_loop
;; if we get here, we didn't find a zero val
ret
.found_a_zero_val:
;; do something with the element referred to by `rdx`
注意我们必须使用movzx
,因为有效地址中的所有寄存器必须大小相同。 (所以word [storage + al*2]
不起作用。)
另一方面,在将mov [storage + rdx*2], ax
转换为next
之后,将al
之类的商店的两个字段存储起来,以及{ {1}} val
,可能来自不同的来源。 (如果您还没有在另一个寄存器中使用它,则可能需要使用常规字节加载而不是movzx)。这不是什么大问题:不要让你的代码难以阅读或更复杂,以避免做两个字节存储。至少,直到你发现store-port uops是某个循环中的瓶颈。
在数组中使用索引而不是指针可以节省大量空间,尤其是。在64位系统上,指针占用8个字节。如果您不需要释放单个节点(即数据结构只会增长,或者在删除时一次性删除),那么新节点的分配器是微不足道的:只需将它们粘在数组的末尾,和ah
。或者使用c ++ realloc(3)
。
使用这些构建块,您应该全部设置为实现通常的链表algos。只需使用std::vector
或其他任何内容存储字节。
如果您需要有关如何使用干净算法实现链接列表的想法,这些算法可以处理尽可能少的分支的所有特殊情况,请查看this Codereview question。它适用于Java,但我的答案非常C风格。其他答案也有一些不错的技巧,其中一些我借用了我的答案。 (例如,使用虚拟节点避免分支处理插入新头特殊情况。)