誰かがレジスタに値を10進形式で表示するための純粋にアセンブリコードを教えてもらえますか? printfハックの使用を勧めず、gccでコンパイルしてください。
説明:
まあ、私はいくつかの研究とNASMでの実験を行い、cライブラリのprintf関数を使用して整数を出力できると考えました。オブジェクトファイルをGCCコンパイラーでコンパイルすることでそうしましたが、すべてが十分に機能します。
しかし、私が達成したいのは、レジスタに格納されている値を10進数形式で出力することです。
私はいくつかの調査を行い、2または9がahレジスターにあり、データがdxにある間、DOSコマンドラインの割り込みベクトル021hが文字列と文字を表示できると考えました。
結論:
私が見つけた例はどれも、Cライブラリのprintfを使用せずにレジスタの内容値を10進形式で表示する方法を示していません。アセンブリでこれを行う方法を誰かが知っていますか?
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
読者への課題として残しました:先行ゼロを抑制します。また、数字の文字をメモリに書き込むため、スタックに書き込む代わりに、バッファに書き込んでから、バッファの内容を出力することもできます。また、読者への演習として残しました。
バイナリ整数を手動で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構文で整数を文字列として出力しました。パフォーマンスに関するコメント、およびdiv
とmul
を使用したコンパイラ生成コードのベンチマークについては、それを参照してください。
関連: NASMアセンブリ入力を整数に変換しますか? は反対方向です。
コメントできないので、この方法で返信を投稿します。 @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を印刷する必要があります。