【问题标题】:Conversion of huge decimal numbers (128bit) formatted as ASCII to binary (hex)将 ASCII 格式的大十进制数(128 位)转换为二进制(十六进制)
【发布时间】:2019-11-29 16:07:57
【问题描述】:

我希望这将是我关于这个主题的最后一个问题!

我正在寻找一种方法来将编码为 ASCII 的巨大十进制数字转换为它们的 128 位十六进制(二进制)表示。

这些实际上是以十进制表示的 IPv6 地址。

例如:“55844105986793442773355413541572575232”解析为:0x2a032f000000000000000000000000000

我的大部分代码都在 x86-32 MASM 程序集中,所以我宁愿保持这种方式也不愿在不同语言之间切换。

我有在 python 中工作的代码,但如上所述,我希望所有内容都在 x86 asm 中。

【问题讨论】:

  • 二进制 -> 十六进制很容易;每个字节分别转换为两个十六进制数字。 (或每个 dword 到 8 个十六进制数字)。请参阅How to convert a number to hex? 了解简单循环,以及使用 SSE2、SSSE3、AVX2、AVX512F 或 AVX512VBMI 一次将 64 位输入转换为 16 字节十六进制的有效方法,或者使用 AVX2 甚至执行整个 128 位输入一步到位。
  • 您是在编写完全汇编代码,还是从 C/C++/Python 等高级语言调用汇编代码?
  • @MichaelPetch,我完全在使用程序集(WinASM Studio),或者至少尝试这样做!我的概念证明是在 Python 中
  • 我从事事件响应工作,并且我编写了一个工具来解析 Office 365 审核日志(已完成并正常工作)。除此之外,我现在正在构建一个数据集,以针对国家和代理交叉引用 IP 地址。 IPv4 很简单,只是 V6 的地址让我头疼,所以,在阅读了这里的其他 cmets 和代码后,我不得不睡一觉!
  • IDK 为什么首先要将 IPv6 地址作为单个十进制整数,或者为什么要使用 asm 解析它。特别是 32 位模式的 asm。如果您正在开发新工具,为什么不让它们在只需要 2 个寄存器且保证 SSE2 支持的 64 位模式下运行?您通常可以让 C 编译器(如 gcc)处理 128 位整数。

标签: assembly x86 bigint


【解决方案1】:

这是两部分 - 将“十进制 ASCII”转换为 128 位无符号整数;然后将 128 位无符号整数转换为“十六进制 ASCII”。

第一部分是这样的:

set result to zero
for each character:
    if character not valid handle "invalid character error" somehow
    else
        if result is larger than "max/10" handle "overflow error" somehow
        result = result * 10
        digit = character - '0'
        if result is larger than "max - digit" handle "overflow error" somehow
        result = result + digit

为此,您需要将 128 位整数乘以 10、比较两个 128 位整数、从 128 位整数中减去一个字节以及将一个字节添加到 128 位整数的代码。乘以 10;它可以(并且应该)实现为“x = (x << 3) + (x << 1)”;所以可以认为是左移和加法。

注意:我假设 32 位 80x86(基于您之前的问题)。我还将使用 NASM 语法(因为我对 MASM 语法“不太熟悉”),但转换为 MASM 语法应该很容易

左移;您可以将 128 位整数分成 4 个(32 位)块并使用类似:

    ;esi = address of source number
    ;edi = address of destination number
    ;cl = shift count

    mov edx,[esi+12]
    mov eax,[esi+8]
    shld edx,eax,cl
    mov [edi+12],edx
    mov edx,eax
    mov eax,[esi+4]
    shld edx,eax,cl
    mov [edi+8],edx
    mov edx,eax
    mov eax,[esi]
    shld edx,eax,cl
    mov [edi+4],edx
    shl eax,cl
    mov [edi],eax

两个 128 位数字相加:

    ;esi = address of first source number
    ;edi = address of second source number and destination

    mov eax,[esi]
    add [edi],eax
    mov eax,[esi+4]
    adc [edi+4],eax
    mov eax,[esi+8]
    adc [edi+8],eax
    mov eax,[esi+12]
    adc [edi+12],eax

将 dword(零扩展字节)添加到 128 位数字:

    ;eax = first number
    ;edi = address of second number and destination

    add [edi],eax
    adc dword [edi+4],0
    adc dword [edi+8],0
    adc dword [edi+12],0

用于从 128 位数字中减去 dword(零扩展字节):

    ;eax = first number
    ;edi = address of second number and destination

    sub [edi],eax
    sbb dword [edi+4],0
    sbb dword [edi+8],0
    sbb dword [edi+12],0

用于比较 128 位整数:

    ;esi = address of first source number
    ;edi = address of second source number

    mov eax,[esi+12]
    cmp [edi+12],eax
    jb .smaller
    ja .larger
    mov eax,[esi+8]
    cmp [edi+8],eax
    jb .smaller
    ja .larger
    mov eax,[esi+4]
    cmp [edi+4],eax
    jb .smaller
    ja .larger
    mov eax,[esi]
    cmp [edi],eax
    jb .smaller
    ja .larger
    mov al,0         ;Values are equal
    ret

.smaller:
    mov al,-1        ;First value is smaller than second
    ret

.larger:
    mov al,1         ;First value is larger than second
    ret

第二部分(转换为十六进制 ASCII)相当简单——主要是“从最高到最低的每个字节;将字节转换为 2 个十六进制字符(可能使用查找表)”的事情。你应该可以很容易地找到代码来做到这一点,所以我不会在这里描述它。

【讨论】:

  • shld by cl 在当前 Intel 上的效率远低于 shld r,r, 13。就像 4 微指令而不是 1。(即便如此,shld 即时仍然有 3 个周期的延迟,并且每时钟只有 1 个吞吐量,不幸的是。)所以很容易用 add/ 3xadc 链,特别是如果您在 Haswell 之前不关心英特尔。
  • @PeterCordes:目标(尤其是对于初学者)是理解和维护代码的能力,而不是性能。
  • 您在指针上花费了大量寄存器。我将处理相对于 ESP 的 tmp 缓冲区,以保留至少 4 个寄存器来保存我的 128 位总数。在计算total * 10 期间溢出似乎是不可避免的,除非我们使用mulxmul。但是您只需要溢出它的 1 个副本,例如在 4x 寄存器中移位 1,然后将其溢出并将寄存器再移位 2,然后从内存中添加 / adc。留出空间将输入字符串指针保存在另一个寄存器中。
  • 我在答案中添加了代码示例。我认为泄漏total*2 会导致代码非常可读和易于理解,并且效率很高。
【解决方案2】:

Hex 是二进制的 ASCII 序列化格式。您首先要在寄存器中从 ASCII 十进制转换为二进制整数。 然后将该二进制转换为十六进制。 十六进制!= 二进制。


二进制 -> 十六进制很容易;每个二进制字节分别转换为两个 ASCII 十六进制数字。 (或每个 dword 到 8 个十六进制数字)。请参阅How to convert a binary integer number to a hex string? 了解简单循环,以及使用 SSE2、SSSE3、AVX2、AVX512F 或 AVX512VBMI 一次将 64 位输入转换为 16 字节十六进制的有效方法,或者使用 AVX2 甚至可以执行整个 128 位 /一步输入 16 字节,生成所有 32 字节的十六进制数字。


这就留下了 decimal-ASCII -> unsigned __int128 输入 问题。使用shld/.../shl(从高位 dword 开始)和添加 add/adc/adc/adc(从低位 dword 开始)的 128 位移位很简单,因此您可以实现通常的 total = total * 10 + digit (@ 987654322@) 但具有扩展精度的 128 位整数数学。保存一个 128 位整数需要 4 个 32 位寄存器。

t*10 实现为t*2 + t*8 = (t*2) + (t*2)*4,首先使用3x shldadd eax,eaxadd eax,eax + 3x adc same,same 加倍。然后复制并移动另一个 2,然后将两个 128 位数字相加。

但是只有 7 个 GP 整数寄存器(不包括堆栈指针),您必须将某些内容溢出到内存中。而且你还希望你的字符串输入指针在一个寄存器中。

因此,您可能希望在 4x 寄存器中左移 1,然后将它们溢出到内存并在寄存器中再移动 2。然后 add/3xadc 从您溢出它们的堆栈缓冲区中。 这使您可以将 4 个 reg 中的 128 位整数乘以 10,而无需使用任何额外的寄存器。

    ; input:  total = 128-bit integer in  EBX:ECX:EDX:EAX
     ; 16-byte tmp buffer at [esp]
    ; result: total *= 10  in-place
    ; clobbers: none

    ; it's traditional to keep a 64-bit integer in EDX:EAX, e.g. for div or from mul
    ; I chose EBX:ECX for the high half so it makes an easy-to-remember pattern.

;;; total *= 2  and copy to tmp buf
    add   eax, eax             ; start from the low element for carry propagation
    mov   [esp + 0], eax
    adc   edx, edx
    mov   [esp + 4], edx
    adc   ecx, ecx
    mov   [esp + 8], ecx
    adc   ebx, ebx
    mov   [esp + 12], ebx

;;; shift that result another 2 to get   total * 8
    shld  ebx, ecx, 2        ; start from the high element to pull in unmodified lower bits
    shld  ecx, edx, 2
    shld  edx, eax, 2
    shl   eax, 2

;;; add total*2 from memory to total*8 in regs to get  total*10
    add   eax, [esp + 0]
    adc   edx, [esp + 4]
    adc   ecx, [esp + 8]
    adc   ebx, [esp + 12]

乱序执行在这里非常很有帮助。请注意,在shld 块中,指令实际上依赖于之前的shld。他们从未修改的较低元素中提取位。只要第一个add eax,eax 运行,shl eax,2 就可以运行(如果前端已经发出)。

寄存器重命名使运行 SHL 成为可能,而不会因 WAR(读后写)危险而停滞。 shld edx, eax, 2 也需要 EAX 作为输入,但寄存器重命名的重点是让 CPU 跟踪 EAX 的版本与 shl eax,2 的输出分开。

这使我们可以编写不使用很多架构寄存器(仅这 4 个)的代码,但仍然可以利用更多物理寄存器来让 shld/shl 块以与程序顺序相反的顺序执行,因为输入已准备好来自 add/adc 块。

这很棒,因为这意味着最终的 add/adc 块(从内存中添加)按其需要的顺序准备好输入,而无需序列化任一指令链的延迟。这很好,因为shld 在当前的 Intel CPU(如 Haswell/Skylake)上具有 3 个周期延迟,而在 Sandybridge/IvyBridge 上是 1 个。 (在 Nehalem 和更早的版本上,它是一个 2-uop 指令,具有 2c 延迟)。但是在 Haswell/Skylake 上,它仍然是 1 uop,每时钟 1 个吞吐量。 (仅限端口 1)

Ryzen 的 shld 较慢:6 微指令,3 个周期延迟,每 3 个周期一个吞吐量。 (https://agner.org/optimize/)

我们可以有效地同时运行 3 个 add 或 shift 链,即使按照程序顺序,每个块都是单独完成的。一旦我们在第 4 个区块中添加新数字,它也可以在飞行中。

示例循环。输入 EBX:ECX:EDX = 0 和 EAX = 第一个数字,准备检查第二个字符是否为数字,然后执行total = t*10 + digit

.digit_loop:
    ... earlier block    ; total *= 10

    add    eax, ebp      ; total += digit
    adc    edx, 0
    adc    ecx, 0
    adc    ebx, 0

.loop_entry_point:
    inc    esi
    movzx  ebp, byte ptr [esi]    ; load a new input digit

    sub    ebp, '0'               ; ASCII digit -> 0..9 integer
    cmp    ebp, 9                 ; unless it was out of range
    jbe   .digit_loop
;else fall through on a non-digit.

; ESI points at the first non-digit
; EBX:ECX:EDX:EAX holds the 128-bit binary integer.

您可以将 total += digit 移动到重新加载 total*2 之前,以更好地隐藏存储转发延迟。


另一个可能的选项是 4x mul 和部分产品的必需 add/adc。如果您可以假设mulx 的 BMI2 在不影响标志的情况下相乘,那可能会很好,这样您就可以将 mulx 与 adc 交错。但是你需要10 在一个寄存器中。

另一种选择是将 XMM 寄存器用于 SSE2 64 位整数数学。或 MMX 用于 64 位 MMX regs。但是,处理 64 位元素边界并不方便,因为只有标量整数才有加法进位。但可能仍然值得,因为您只有一半的操作。


最好将 9 位整数组转换为 32 位十进制,然后进行扩展精度乘以 1e9 进行组合。 (如最后 9 位数字、之前的 9 位数字等)因此您没有为 每个 数字执行所有这些 adc / store+reload 工作。这意味着最后需要进行大量的乘法运算以组合最多四(?)组数字。

或者也许只是用一个寄存器处理前 9 位数字(正常方式),然后用第二个循环扩大到两个寄存器,然后在第 18 位之后扩大到四个数字。这对于结果小于 9 位的数字非常有用,并且只使用快速 1 寄存器累加器。

【讨论】:

  • 谢谢各位。我正在查看代码,但我似乎无法弄清楚从 ASCII 十进制到 128 位原始二进制(十六进制)的转换发生在哪里。可能值得一提的是,源字符串可以是可变长度的,但是当转换为十六进制时,生成的十六进制数不会超过 128 位。结果不会被保存为 ASCII,因此不需要像 Brandon 所说的那样将 bacjk 进一步转换为 ASCII,无论如何这都是微不足道的。
  • 布兰登,不是布兰登,对不起!
  • @colinr :十六进制是二进制整数的序列化格式。你说你想要十六进制。但无论如何,我的代码中的循环在到达一个非数字时结束,寄存器中有 128 位二进制整数。它不会尝试在t = t*10 + digit 循环中进行溢出检测,因此数字不能超过 128 位是很好的。我更新了我的答案以评论循环的结束。如果你确实想把它变成十六进制,你会在之后这样做。如果您不了解字符串 -> 整数循环的基础知识,请阅读解释简单 32 位案例基础知识的链接问题。
  • "shrld" 是一个错误。
猜你喜欢
  • 2012-11-14
  • 1970-01-01
  • 2012-06-26
  • 1970-01-01
  • 1970-01-01
  • 2010-10-15
  • 2012-01-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多