【问题标题】:Assembly: Creating an array of linked nodes装配:创建链接节点数组
【发布时间】:2015-12-02 00:56:41
【问题描述】:

首先,如果由于我没有提供任何代码或没有自己做任何思考而导致这个问题不合适,我很抱歉,我将删除这个问题。

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

【问题讨论】:

  • 在 32 位环境中,指针是 32 位的,即DWORD 大小。您可以像任何其他DWORD 一样加载它们。不确定你想用ecx 做什么,你不能索引一个链表(除非你知道它被存储为一个数组,但这有点离题了)。如果您需要实际的语法帮助,请指定您正在使用的汇编程序。
  • 如果不够清楚,我深表歉意,但我确实声明它将是一个节点数组。那不允许我对数组进行索引吗?
  • 是的,很明显你会将它存储在一个数组中,但我认为你不应该使用这些知识,而是使用普通的链表机制(即遍历 next链接)。即使节点在数组中,它们也可能不按顺序排列,或者可能并非所有插槽都有效。
  • 我明白了......所以我会做mov linked_node.next, PTR linked_list?抱歉,我对 Assembly 完全陌生(只学习了大约 2 周左右)。
  • 要添加一个节点,您需要将next 链接设置为下一个可用存储,在您的情况下,这确实是索引数组槽。我的意思是,为了遍历列表,你不应该索引(可能 - 也许你被允许)。

标签: arrays assembly linked-list


【解决方案1】:

以您熟悉的语言来考虑您的设计。最好是 C,因为 C 中的指针和值是直接映射到 asm 的概念。

假设您想通过存储指向头元素的指针来跟踪您的链表。

#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)]。但是,如果您不小心,您可能会得到更多难以阅读的代码。

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 位 reg 而不是完整的 64 位 rax 来避免更短的指令编码。如果该值为负数,我们将不会使用rax 作为地址的一部分。我们只是使用movsx 将寄存器的其余部分归零,因为mov al, [storage + 2*rdx] 会将rax 的高56 位保留为旧内容。

另一种方法是使用movzx eax, byte [...] / test al, al,因为 8 位 test 的编码和执行速度与 32 位 test 指令一样快。此外,在 AMD Bulldozer 系列 CPU 上,movzx 作为负载的延迟比movsx 低一个周期(尽管它们仍然采用整数执行单元,不像英特尔,movsx/zx 完全由负载端口处理)。

无论哪种方式,movsxmovzx 都是加载 8 位数据的好方法,因为您可以避免在写入部分 reg 后读取完整 reg 和/或错误依赖(在前面reg 的高位内容,即使 知道你已经将它归零,CPU 硬件仍然必须跟踪它)。除非您知道您没有针对 Intel pre-Haswell 进行优化,否则您不必担心部分寄存器写入。 Haswell 进行双重簿记或其他操作,以避免在阅读时将部分值与旧的完整值合并。 AMD CPU、P4 和 Silvermont 不会将部分注册与完整注册分开跟踪,因此您只需要担心错误的依赖关系。


还请注意,您可以将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] 不起作用。)

这可能更有用,在将next 转换为al 并将val 转换为ah 之后,将节点的两个字段存储在一个单独的存储中,例如mov [storage + rdx*2], ax 或其他东西,可能来自不同的来源。 (在这种情况下,如果您还没有将它放在另一个寄存器中,您可能希望使用常规字节加载,而不是 movzx)。这没什么大不了的:不要为了避免进行两个字节存储而使您的代码难以阅读或更复杂。至少,直到您发现存储端口 uops 是某个循环中的瓶颈。


使用数组的索引而不是指针可以节省大量空间,尤其是。在指针占用 8 个字节的 64 位系统上。如果您不需要释放单个节点(即数据结构只会增长,或者在删除时立即全部删除),那么新节点的分配器是微不足道的:只需将它们粘贴在数组的末尾,和realloc(3)。或者使用 c++ std::vector


有了这些构建块,您应该已经准备好实现通常的链表算法了。只需使用 mov [storage + rdx*2], al 或其他方式存储字节即可。

如果您需要有关如何使用干净算法实现链表的想法,以处理尽可能少的分支的所有特殊情况,请查看this Codereview question。它适用于 Java,但我的回答非常 C 风格。其他答案也有一些不错的技巧,其中一些是我借来的。 (例如,使用虚拟节点避免分支来处理插入作为新头的特殊情况)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多