汇编:创建链接节点数组

时间:2015-12-02 00:56:42

标签: arrays assembly linked-list

首先,如果这个问题不合适,因为我没有提供任何代码或不自行思考,我道歉,我会删除这个问题。

对于赋值,我们需要创建一个节点数组来模拟链表。每个节点都有一个整数值和一个指向列表中下一个节点的指针。这是我的.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?或者我错误地想到了这个?任何帮助将不胜感激!

1 个答案:

答案 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]。下一个节点valstorage[p.next].val

让我们看看asm中的内容。 NASM manual讨论了它的宏系统如何帮助你使用全局结构使代码更具可读性,但我没有为此做过任何宏的事情。您可以为NEXTVAL或其他内容定义宏,其中包含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完全处理通过加载端口)。

无论哪种方式,movsxmovzx都是加载8位数据的好方法,因为您可以避免在写入部分注册表和/或错误依赖项后读取完整注册表的问题(在reg的高位的先前内容上,即使知道你已将其归零,CPU硬件仍然必须跟踪它)。除非您知道自己没有针对英特尔前Haswell进行优化,否则您不必担心部分寄存器写入。 Haswell执行双簿记或者某些东西以避免额外的uops在阅读时将部分值与旧的完整值合并。 AMD CPU,P4和Silvermont不会与full-reg分开跟踪部分注册表,所以你只需要担心错误依赖。

另请注意,您可以加载nextval打包在一起,例如

.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风格。其他答案也有一些不错的技巧,其中一些我借用了我的答案。 (例如,使用虚拟节点避免分支处理插入新头特殊情况。)