web-dev-qa-db-ja.com

Cライブラリからprintfを使用せずに、アセンブリレベルプログラミングで整数を出力するにはどうすればよいですか?

誰かがレジスタに値を10進形式で表示するための純粋にアセンブリコードを教えてもらえますか? printfハックの使用を勧めず、gccでコンパイルしてください。

説明:

まあ、私はいくつかの研究とNASMでの実験を行い、cライブラリのprintf関数を使用して整数を出力できると考えました。オブジェクトファイルをGCCコンパイラーでコンパイルすることでそうしましたが、すべてが十分に機能します。

しかし、私が達成したいのは、レジスタに格納されている値を10進数形式で出力することです。

私はいくつかの調査を行い、2または9がahレジスターにあり、データがdxにある間、DOSコマンドラインの割り込みベクトル021hが文字列と文字を表示できると考えました。

結論:

私が見つけた例はどれも、Cライブラリのprintfを使用せずにレジスタの内容値を10進形式で表示する方法を示していません。アセンブリでこれを行う方法を誰かが知っていますか?

16

2進数から10進数への変換ルーチンを作成し、10進数を使用して「数字」を生成して印刷する必要があります。

どこかで、選択した出力デバイスに文字が印刷されると想定する必要があります。このサブルーチンを「print_character」と呼びます。 EAXで文字コードを取り、すべてのレジスターを保持することを前提としています(そのようなサブルーチンがない場合は、別の質問の基礎となる追加の問題があります)。

レジスタ(たとえば、EAX)に数字のバイナリコード(たとえば、0〜9の値)がある場合、ASCIIコードを追加して、その値を数字の文字に変換できます。レジスターへの「ゼロ」文字。これは次のように簡単です。

       add     eax, 0x30    ; convert digit in EAX to corresponding character digit

次に、print_characterを呼び出して、数字の文字コードを出力できます。

任意の値を出力するには、数字を選択して印刷する必要があります。

基本的に数字を選択するには、10の累乗での作業が必要です。 10の1のべき乗、たとえば10自体で作業するのが最も簡単です。 EAXで値を取り、EDXで商を生成し、EAXで剰余を生成する10で割るルーチンがあるとします。そのようなルーチンを実装する方法を理解するための演習として残します。

次に、正しい考えの単純なルーチンは、値が持つ可能性のあるすべての桁に対して1桁を生成することです。 32ビットのレジスターは40億までの値を格納するため、10桁が印刷される場合があります。そう:

         mov    eax, valuetoprint
         mov    ecx, 10        ;  digit count to produce
loop:    call   dividebyten
         add    eax, 0x30
         call   printcharacter
         mov    eax, edx
         dec    ecx
         jne    loop

これは機能しますが、数字を逆順に出力します。おっとっと!さて、プッシュダウンスタックを利用して、生成された数字を格納し、逆の順序でポップすることができます。

         mov    eax, valuetoprint
         mov    ecx, 10        ;  digit count to generate
loop1:   call   dividebyten
         add    eax, 0x30
         Push   eax
         mov    eax, edx
         dec    ecx
         jne    loop1
         mov    ecx, 10        ;  digit count to print
loop2:   pop    eax
         call   printcharacter
         dec    ecx
         jne    loop2

読者への課題として残しました:先行ゼロを抑制します。また、数字の文字をメモリに書き込むため、スタックに書き込む代わりに、バッファに書き込んでから、バッファの内容を出力することもできます。また、読者への演習として残しました。

15
Ira Baxter

バイナリ整数を手動でASCII 10進数の文字列/配列に変換する必要があります。ASCII数字は、_'0'_(0x30)から_'9'_(0x39)の範囲の1バイト整数で表されます。 http://www.asciitable.com/

16進数のような2の累乗の基数については、「 数値を16進数に変換する方法」を参照してください。 バイナリと2の累乗のベースの間の変換では、ビットの各グループが16進数/ 8進数に個別にマップされるため、さらに多くの最適化と単純化が可能になります。


ほとんどのオペレーティングシステム/環境には、整数を受け入れてそれらを10進数に変換するシステムコールがありません。 OSにバイトを送信したり、ビデオメモリにバイトをコピーしたり、ビデオメモリに対応するフォントグリフを描画したりする前に、自分で行う必要があります...

8バイトを書き込むシステムコールは基本的に1バイトを書き込むのと同じコストであるため、これまでで最も効率的な方法は、文字列全体を一度に実行する単一のシステムコールを作成することです。

これはバッファが必要であることを意味しますが、それは私たちの複雑さをまったく増しません。 2 ^ 32-1は4294967295のみで、10進数で10桁です。バッファを大きくする必要はないので、スタックを使用できます。

通常のアルゴリズムでは、LSDファースト(最下位桁が最初)の数字が生成されます。印刷順序はMSDファーストであるため、バッファの最後から開始して、逆方向に作業できます。他の場所で印刷またはコピーする場合は、開始位置を追跡し、固定バッファの開始位置に到達することを気にしないでください。何かを元に戻すためにプッシュ/ポップをいじる必要はありません。最初から逆方向に生成するだけです。

_char *itoa_end(unsigned long val, char *p_end) {
  const unsigned base = 10;
  char *p = p_end;
  do {
    *--p = (val % base) + '0';
    val /= base;
  } while(val);                  // runs at least once to print '0' for val=0.

  // write(1, p,  p_end-p);
  return p;  // let the caller know where the leading digit is
}
_

gcc/clangは、divの代わりに マジック定数乗数 を使用して、10で効率的に除算する優れた仕事をします。 ( Godboltコンパイラエクスプローラ asm出力用)。


符号付き整数を処理するには:

このアルゴリズムを符号なし絶対値に使用します。 (if(val<0) val=-val;)。元の入力が負の場合は、最後に_'-'_を最後に貼り付けます。したがって、たとえば、_-10_は、これを_10_で実行し、2 ASCIIバイトを生成します。次に、_'-'_を、ストリング。


これは、32ビットの符号なし整数にdiv(遅いが短いコード)とLinuxのwriteシステムコールを使用した、コメント付きの簡単なNASMバージョンです。 これをレジスタをecxではなくrcxに変更するだけで、これを32ビットモードのコードに簡単に移植できます。ただし、_add rsp,24_は_add esp, 20_になります。これは、_Push ecx_が8バイトではなく4バイトであるためです(これを行わない限り、通常の32ビット呼び出し規約ではesiも保存/復元する必要があります)マクロまたは内部使用専用関数に挿入します。)

システムコール部分は、64ビットLinuxに固有です。これをシステムに適したものに置き換えてください。 32ビットLinuxでの効率的なシステムコールのためにVDSOページを呼び出すか、非効率的なシステムコールのために_int 0x80_を直接使用します。 Unix/Linuxでの32ビットおよび64ビットシステムコールの 呼び出し規約 を参照してください。

文字列を印刷せずに必要なだけの場合rsiはループを終了した後の最初の数字を指します。 tmpバッファーから実際に必要な場所にコピーすることができます。または、それを最終的な宛先に直接生成した場合(例えば、ポインターargを渡す)、残したスペースの前に到達するまで、先行ゼロを埋め込むことができます。常に固定幅までゼロを埋めない限り、開始前に何桁あるかを知る簡単な方法はありません。

_ALIGN 16
; void print_uint32(uint32_t edi)
; x86-64 System V calling convention.  Clobbers RSI, RCX, RDX, RAX.
global print_uint32
print_uint32:
    mov    eax, edi              ; function arg

    mov    ecx, 0xa              ; base 10
    Push   rcx                   ; newline = 0xa = base
    mov    rsi, rsp
    sub    rsp, 16               ; not needed on 64-bit Linux, the red-zone is big enough.  Change the LEA below if you remove this.

;;; rsi is pointing at '\n' on the stack, with 16B of "allocated" space below that.
.toascii_digit:                ; do {
    xor    edx, edx
    div    ecx                   ; edx=remainder = low digit = 0..9.  eax/=10
                                 ;; DIV IS SLOW.  use a multiplicative inverse if performance is relevant.
    add    edx, '0'
    dec    rsi                 ; store digits in MSD-first printing order, working backwards from the end of the string
    mov    [rsi], dl

    test   eax,eax             ; } while(x);
    jnz  .toascii_digit
;;; rsi points to the first digit


    mov    eax, 1               ; __NR_write from /usr/include/asm/unistd_64.h
    mov    edi, 1               ; fd = STDOUT_FILENO
    lea    edx, [rsp+16 + 1]    ; yes, it's safe to truncate pointers before subtracting to find length.
    sub    edx, esi             ; length, including the \n
    syscall                     ; write(1, string,  digits + 1)

    add  rsp, 24                ; (in 32-bit: add esp,20) undo the Push and the buffer reservation
    ret
_

パブリックドメインこれをコピーして、作業しているものに貼り付けてください。壊れた場合は、両方のピースを保持できます。

そして、これは0(0を含む)までカウントダウンするループで呼び出すコードです。同じファイルに入れると便利です。

_ALIGN 16
global _start
_start:
    mov    ebx, 100
.repeat:
    lea    edi, [rbx + 0]      ; put +whatever constant you want here.
    call   print_uint32
    dec    ebx
    jge   .repeat


    xor    edi, edi
    mov    eax, 231
    syscall                             ; sys_exit_group(0)
_

組み立ててリンクする

_yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm &&
ld -o print-integer print-integer.o

./print_integer
100
99
...
1
0
_

straceを使用して、このプログラムが行うシステムコールがwrite()exit()のみであることを確認します。 ( x86 タグwikiの下部にあるgdb /デバッグのヒント、およびその他のリンクも参照してください。)


64ビット整数のAT&T構文バージョンを への回答として投稿しました。printf の代わりにLinuxシステムコールを使用して、AT&T構文で整数を文字列として出力しました。パフォーマンスに関するコメント、およびdivmulを使用したコンパイラ生成コードのベンチマークについては、それを参照してください。


関連NASMアセンブリ入力を整数に変換しますか? は反対方向です。

4
Peter Cordes

コメントできないので、この方法で返信を投稿します。 @Ira Baxter、完璧な回答投稿したレジスタcxを値10に設定すると、投稿時に10で除算する必要がないことを追加したいと思います。「ax == 0」になるまで、axで数値を除算するだけです。

loop1: call dividebyten
       ...
       cmp ax,0
       jnz loop1

また、元の数で何桁あったかを保存する必要があります。

       mov cx,0
loop1: call dividebyten
       inc cx

とにかく、Ira Baxterが私を助けてくれて、コードを最適化する方法はいくつかあります:)

これは、最適化だけでなく、フォーマットについてもです。番号54を印刷する場合、0000000054ではなく54を印刷する必要があります。

1
Gondil