创建将整数转换为 0 和 1 的 16 位二进制字符串的 x86 汇编程序

Creating an x86 assembler program that converts an integer to a 16-bit binary string of 0's and 1's

正如问题所暗示的,我必须编写一个 MASM 程序来将整数转换为二进制。我尝试了很多不同的方法,但其中 none 对我有帮助。我正在处理的最终代码如下。当我调试 Visual Studio 中的代码时,出现访问内存冲突错误。

任何关于如何解决错误的帮助,以及我是否在正确的轨道上,将不胜感激。第一个代码是我的 C++ 代码,它将一个 char 数组传递给 .asm 文件以转换为二进制文件。

#include <iostream>
using namespace std;
extern "C"
{
  int intToBin(char*);
}

int main()
{
  char str[17] = { NULL };
  for (int i = 0; i < 16; i++)
  {
    str[i] = '0';
  }

  cout << "Please enter an integer number :";
  cin >>str;
  intToBin(str);
  cout << " the equivilant binaryis: " << str << endl;
  return 0;
}

.asm 文件如下:

.686
.model small
.code 

_intToBin PROC       ;name of fucntion
  start:    

    push ebp ; save base pointer
    mov ebp, esp ; establish stack frame

    mov eax, [ebp+8] ; stroing char value into eax
    mov ebx, [ebp+12]; adress offset of char array
    mov edx,32768 ;storin max 16bit binary in edx
    mov ecx,17  ; since its a 16 bit , we do the loop 17 times


  nextBite:
    test eax,edx        ;testing if eax is equal to edx
    jz storeZero        ;if it is 0 is to be moved into bl

    mov bl,'1'          ;if not 1 is moved into bl
    jmp storeAscBit     ;then jump to store ascii bit

  storeZero:
    mov bl,'0'          ;moving 0 into bl register

  storeAscBit:
    mov [di ],bl        ;moving bl (either 1 or 9) into [di]
    inc edx             ;increasing edx stack by 1 point to go to next bt
    shr edx,1           ;shfiting right 1 time so the 1 comes to second      
    loop nextBite       ; do the whole step again

  EndifReach:   
    pop ebp
_intToBin ENDP
 END

这是解释一些术语的高级答案。

第 1 部分 - 关于整数及其在计算机中的编码

整数值就是整数值,在数学上是纯粹抽象的东西。数字“5”不是您在显示器上看到的(那是数字 5(图形图像或 "glyph")代表可以识别该字形的人类(和一些训练有素的动物)以 base-10(十进制)格式表示的值 5模式;值 5 本身纯粹是 抽象 ).

当你在 C++ 中使用 int 时,它并不是完全抽象的,它更像是硬连接到金属中。它是 32 位(在大多数平台上)整数值。

但与将其想象为人类十进制格式相比,这种抽象描述仍然更接近事实。

int a = 12345; // decimal number

此处 a 包含值 12345,而不是格式。它不知道它在源代码中是作为十进制字符串输入的。

int a = 0x3039; // hexadecimal number

将编译成完全 相同的 机器代码,对于 CPU 来说是一样的,仍然是 (a == 12345)。最后:

int a = 0b0011000000111001; // binary number

又是一回事。它仍然是相同的 12345 值,只是格式不同。

最后一种二进制形式最接近 CPU 用来存储值的形式。它以 32 位存储(low/high 电压 cells/wires),因此如果您测量特定 cell/wire 上的电压,您会在前 18 位上看到“0”电压电平,然后 2具有“1”电压电平的位,然后其余的就像上面的二进制格式一样......两个最低有效位是“0”和“1”。

另外 大多数 CPU 电路不知道特定位的特定值,这又是那个 0/1 的 "interpretation",由代码。许多 CPU 算法,如 addsub 在所有位上工作 "from right to left",不知道当前处理的位代表最终整数值,例如 2 13 值(第 14 个最低有效位)。

它是在获取这些位,并计算具有这些位值的 decimal/hexadecimal/binary 表示的字符串时,当您为这些“1”赋予它们的值时。那么它就变成了 text "12345".

如果您以不同的方式处理这 32 位,例如 LED 显示面板的 ON/OFF LED 灯的表示,那么一旦您将它从 CPU 发送到显示器,它就会如此, LED 显示屏会点亮相应的 LED 灯,不管那些位组成 12345 值当被视为 int.

只有极少数 CPU 指令以需要了解特定位的特定值的方式工作。

第 2 部分 - 关于 C/C++ 函数的输入、输出和参数

您想"convert decimal integer (input) to binary."

那么让我们推理一下什么是输入,什么是输出。输入取自 std::cin,因此用户将输入字符串。

但是如果你愿意的话:

int inputNum;
std::cin >> inputNum;

您将以已转换的整数值(32 位,见上文)结束(或无效 std::cin 状态,当用户输入不正确的数字时,可能不是您处理此问题的任务)。

如果您有 int 中的数字,二进制转换已经由 clib 在将用户输入字符串编码为 32 位整数时完成。

现在您可以使用 C 原型创建 asm 函数:

void formatToBinary(uint16_t value, char result[17]);

这意味着你将给它uint16_t(无符号16位)整数值,以及指向内存中17个保留字节的指针,你将在其中写入'0''1' ASCII字符,并用另一个 0 值终止它(对于这个值的粗略描述,请按照我在您问题下的评论中的第一个 link)。

如果必须将输入作为字符串,即。

char str[17];
std::cin > str;

那么你将在 str(输入“12345”之后)个字节中得到值:'1'(十进制为 49)、'2''3''4''5'0。 (注意最后一个是零,不是 ASCII 数字 '0' = 值 48)。

您首先需要将这些 ASCII 字节转换为整数值(在 C++ 中 atoi 可能有帮助,或者 conversions/formatting 的少数其他函数之一)。在 ASM 中检查 SO 的问题 "how to enter integer".

一旦你将它转换为整数值,你可以按照上面描述的相同方式进行(此时它已经编码为 16 或 32 位,因此输出它的字符串表示应该很容易)。

您可能仍然 运行 遇到一些棘手的部分,例如您是否不想输出前导零等...但如果您了解其工作原理,所有这些都应该很容易。

在这种情况下,您的 ASM 函数原型可能只是 void convertToBinary(char*); 以重用字符串指针作为输入和输出。

您的 int intToBin(char*); 看起来很奇怪,因为这意味着 ASM 将 return int .. 但为什么呢?这是整数值,没有绑定到任何特定格式,所以它同时是 binary/octal/decimal/hexa。取决于你如何展示它。所以你不需要它,你只需要二进制形式表示值的字符串,也就是char *。而且你不给它你输入的数字(除非它是从字符串中获取的)。


根据任务描述和您的技能水平,我认为您可以在 C++ 中将输入转换为 int(即 std::cin >> int_variable;)。


顺便说一句,如果您完全了解计算机中的值发生了什么,以及 CPU 指令如何处理它们,您通常可以通过许多不同的方式来实现某些结果。例如 Jose 的二进制转换是用简单的方式写的,就像一个 Assembly 新手会写的那样(他这样写是为了让你更容易理解):

           mov eax, num   // ◄■■ THE NUMBER.
           lea edi, bin   // ◄■■ POINT TO VARIABLE "BIN".
           mov ecx, 32    // ◄■■ NUMBER IS 32 BITS.
        conversion:
            shl eax, 1     // ◄■■ GET LEFTMOST BIT.
            jc  bit1       // ◄■■ IF EXTRACTED BIT == 1
            mov [edi], '0'
            jmp skip
        bit1:
            mov [edi], '1'
        skip :
            inc edi   // ◄■■ NEXT POSITION IN "BIN".
            loop conversion

它仍然有点脆弱,例如他以这样的方式初始化"bin",它包含32个空格并且第33个值是零(C字符串的空终止符)。然后在代码中他确实修改了 32 个字节,所以第 33 个零仍然存在并且有效。如果您要调整他的代码以跳过前导零,它会 "break" 通过显示缓冲区的剩余部分,因为他没有明确设置空终止符。

这是在 Assembly 中编写代码以提高性能的常用方法,可以准确了解发生的一切,而不是设置已经 set/etc 的值。在你学习的时候,我建议你以"defensive"的方式工作,而不是做一些浪费的事情,这样可以在出现错误时起到安全网的作用,所以我会在[=53]之后添加mov byte ptr [edi],0 =] 再次明确设置终止符。

但实际上速度不是很快,因为它使用的是分支。 CPU不喜欢,解码新指令是一个代价高昂的任务,如果不确定执行哪条指令,它只是向前解码一条路径,如果猜错了,它会抛出它out,并解码正确的路径,但这意味着几个周期会暂停执行,直到新路径的第一条指令被完全解码并准备好执行。

所以在编码性能时,你要避免难以预测的分支(最终的 loop 对于 CPU 很容易预测,因为它总是循环,直到最终退出之后ecx 是 0)。在这种情况下,许多可能的方法之一可以是:

   mov edx, num
   lea edi, bin
   mov ah,'0'/2   // for fast init of al later
   // '0' is 48 (even), '0'/2 will work (24)
   mov ecx, 32    // countdown counter
conversion:
   mov al,ah      // al = '0'/2
   shl edx, 1     // most significant bit into CF
   adc al,al      // al = '0'/2 + '0'/2 + CF = '0' or '1'
   stosb          // store the '0' or '1' to [edi++]
   dec ecx        // manually written "loop"
   jnz conversion // (it is faster on modern CPUs)
   mov [edi],ch   // explicit set of null-terminator
       // (ch == 0, because here ecx == 0)

如您所见,现在除了循环之外没有分支,CPU分支预测将处理得更顺畅,性能也会好得多。


用于与 Cody 讨论的双字变体(NASM 语法,32b 目标):

; .data
binNumber   times 36 db 0

; .text
numberToBin:
    mov     edx,0x12345678
    lea     edi,[binNumber]
    mov     ecx, 32/4       ; countdown counter
n2b_conversion:
    mov     eax,0b11000000110000001100000011000
      ; ^ will become '0'/'1' for each of four bits
    shl     edx,1
    rcr     eax,8
    shl     edx,1
    rcr     eax,8
    shl     edx,1
    rcr     eax,8
    shl     edx,1
    rcr     eax,8
      ; here was "or eax,'0000'" => no more needed.
    stosd
    dec     ecx
    jnz     n2b_conversion
    mov     [edi],dl        ; null terminator
    ret

没有分析它,只是验证了它 return 正确的结果。

接下来是使用“atoi”将字符串转为数字,再使用汇编将数字转为二进制的例子:

#include "stdafx.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{   char str[6]; // ◄■■ NUMBER IN STRING FORMAT.
    int num;    // ◄■■ NUMBER IN NUMERIC FORMAT.
    char bin[33] = "                                "; // ◄■■ BUFFER FOR ONES AND ZEROES.
    cout << "Enter a number: ";
    cin >> str;  // ◄■■ CAPTURE NUMBER AS STRING.
    num = atoi(str); // ◄■■ CONVERT STRING TO NUMBER.
    __asm { 
           mov eax, num   // ◄■■ THE NUMBER.
           lea edi, bin   // ◄■■ POINT TO VARIABLE "BIN".
           mov ecx, 32    // ◄■■ NUMBER IS 32 BITS.
        conversion:
            shl eax, 1     // ◄■■ GET LEFTMOST BIT.
            jc  bit1       // ◄■■ IF EXTRACTED BIT == 1
            mov [edi], '0'
            jmp skip
        bit1:
            mov [edi], '1'
        skip :
            inc edi   // ◄■■ NEXT POSITION IN "BIN".
            loop conversion
    }
    cout << bin;
    return 0;
}

十进制字符串 -> 整数部分,参见 NASM Assembly convert input to integer?


您可以在没有任何循环的情况下完成整个事情,使用 SIMD 并行处理所有位。

还相关:

  • 包括标量和 SIMD。

  • int -> decimal string(或其他非 2 的幂数)

  • How to efficiently convert an 8-bit bitmap to array of 0/1 integers with x86 SIMD 一个整洁的 SIMD 版本,当你想用 0/1 整数而不是 ASCII 数字停止时它是有效的。它专门针对 8 位 -> 8 字节进行了优化。

  • - 这个答案的内在版本;包括一个转换为 ASCII '0' / '1' 以及仅 0 / 1 字节的版本,带有 SSE2 / SSSE3 和 AVX-512。

  • How to create a byte out of 8 bool values (and vice versa)? 展示了一个使用 64 位乘法常数的技巧。在 32 位模式下,每 8 位进行两次乘法运算,分别产生低 4 位和高 4 位(来自 imul reg, src, 0x080402010x80402010)。对于每 4 个字节的输出/4 位输入,您需要 and + shr,并且要转换为 ASCII 也需要 add reg, '0000'。但至少你不必分别提取输入的每 4 位,只需使用 movzx 提取 8 位并使用 0x8040201008040201.

    的两半

    总共有很多指令,但如果没有 SSE2 但 imul 不是太慢(例如 Pentium 3 或现代 CPUs,则一次优于 1 位如果您不想依赖 SSE2)。 64 位模式保证 SSE2 可用,或者可以一次使用此标量乘法 8 位 -> 字节。


integer -> base 2 string 部分 比 base10 string->int 更简单,或者至少可以通过一些 SSE2 指令高效地完成并且没有循环。

这使用与 Evgeny Kluev's answer on a question about doing the inverse of PMOVMSKB 相同的技术,将位模式转换为 0 / -1 元素的向量:广播随机播放输入字节,以便每个向量元素都包含您想要的位(加上邻居)。并且只留下零或 1,然后与全零向量进行比较。

此版本仅需要 SSE2,因此它适用于 可以 运行 64 位 OS 的每个 CPU,以及一些仅 32 位 CPUs(如早期的 Pentium4 和 Pentium-M)。它可以通过 SSSE3 运行得更快(一个 PSHUFB 而不是三个洗牌以获得我们想要的低字节和高字节)。您可以使用 MMX 一次执行 8 位 -> 8 字节。

我不会尝试将它从 NASM 转换为 MASM 语法。我已经实际测试过这个,并且有效。 x86 32 位 System V 调用约定与 32 位 Windows cdecl 调用约定在影响此代码的任何方面都没有区别,AFAIK。

;;; Tested and works

;; nasm syntax, 32-bit System V (or Windows cdecl) calling convention:
;;;; char *numberToBin(uint16_t num, char buf[17]);
;; returns buf.

ALIGN 16
global numberToBin
numberToBin:
        movd    xmm0, [esp+4]       ; 32-bit load even though we only care about the low 16 bits.
        mov     eax, [esp+8]        ; buffer pointer

        ; to print left-to-right, we need the high bit to go in the first (low) byte
        punpcklbw xmm0, xmm0              ; llhh      (from low to high byte elements)
        pshuflw   xmm0, xmm0, 00000101b   ; hhhhllll
        punpckldq xmm0, xmm0              ; hhhhhhhhllllllll

        ; or with SSSE3:
        ; pshufb  xmm0, [shuf_broadcast_hi_lo]  ; SSSE3

        pand    xmm0, [bitmask]     ; each input bit is now isolated within the corresponding output byte
        ; compare it against zero
        pxor    xmm1,xmm1
        pcmpeqb xmm0, xmm1          ; -1 in elements that are 0,   0 in elements with any non-zero bit.

        paddb   xmm0, [ascii_ones]  ; '1' +  (-1 or 0) = '0' or 1'

        mov     byte [eax+16], 0    ; terminating zero
        movups  [eax], xmm0
        ret


section .rodata
ALIGN 16

;; only used for SSSE3
shuf_broadcast_hi_lo:
        db 1,1,1,1, 1,1,1,1     ; broadcast the second 8 bits to the first 8 bytes
        db 0,0,0,0, 0,0,0,0     ; broadcast the first 8 bits to the second 8 bytes

bitmask:  ; select the relevant bit within each byte, from high to low for printing
        db 1<<7,  1<<6, 1<<5, 1<<4
        db 1<<3,  1<<2, 1<<1, 1<<0
        db 1<<7,  1<<6, 1<<5, 1<<4
        db 1<<3,  1<<2, 1<<1, 1<<0

ascii_ones:
        times 16 db '1'

在第二个洗牌步骤中使用 PSHUFLW 进行反转在 128b 洗牌速度较慢的旧 CPUs(第一代 Core2 及更早版本)上速度更快,因为仅洗牌低 64 位速度很快。 (与使用 PUNPCKLWD / PSHUFD 相比)。请参阅 Agner Fog 的 Optimizing Assembly guide to learn more about writing efficient asm, and other links in the 标签 wiki。

(Thanks to clang for spotting the possibility).

如果您在循环中使用它,您会将向量常量加载到向量寄存器中,而不是每次都重新加载它们。


来自asm,你可以这样称呼它

    sub     esp, 32

    push    esp           ; buf[] on the stack
    push    0xfba9        ; use a constant num for exmaple
    call    numberToBin
    add     esp, 8
    ;; esp is now pointing at the string

或者使用 asm 注释中的原型从 C 或 C++ 调用它。