プログラムのマシンレベル表現 part3 - 算術演算と論理演算

目次

1. 有効な住所をロードする

2. 整数演算命令

2.1 INC と DEC

2.2 マイナス 

2.3 ADD、SUB、IMUL

3.ブール命令

3.1 かつ

3.2 または

3.3 排他的論理和

3.4 ない

4.シフト操作

4.1 算術左シフトと論理左シフト

4.2 算術右シフトと論理右シフト

5. 特殊な算術演算 


1. 有効な住所をロードする

命令 効果 説明
リーク    S、D D ← &S 実効アドレスのロード

ロード実効アドレスロード実効アドレス) 命令 leaq は movq 命令のバリアントです. 64 ビット システムでは, アドレス長は 64 ビットです. したがって, lea 命令のサイズ サフィックスは q です. 他のバリアントはありませんであり、そのターゲット オペランドはレジスタである必要があります

leaq 命令は非常に特殊です. その一般的な形式はleaq (register) register であり, メモリからレジスタにデータを読み込むように見えます. 実際, leaq は決してメモリを参照しません. つまり, leaq 命令はメモリにアクセスしません.説明する次のプログラム

int main() 
{
    int x = 10;
    int *ptr = &x;

    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 f4 0a 00 00 00 	movl   $0xa,-0xc(%rbp)
  4004f8:	48 8d 45 f4          	lea    -0xc(%rbp),%rax  // 取a的地址放进%rax
  4004fc:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
  400500:	b8 00 00 00 00       	mov    $0x0,%eax
  400505:	5d                   	pop    %rbp
  400506:	c3                   	retq   
  400507:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
  40050e:	00 00 

4004f8: 48 8d 45 f4 lea -0xc(%rbp),%rax

この命令は mov 命令で表現されます。%rbp に格納されている値をベース アドレスとして取得し、アドレスとしてオフセット 0xc を追加し、このアドレスのデータをフェッチして、レジスタ %rax にデータを転送します。

しかし、leaq 命令では、次のことを意味します: %rbp に格納されている値をベース アドレスとして取得し、アドレスとしてオフセット 0xc を追加し、このアドレスをレジスタに転送します。これは & x の動作です

%rbp は、メイン関数のスタック フレームのトップ位置を保持するフレーム レジスタです。

%rbp の値が 10000 で、アドレス 1000c に格納されている値が 10 であるとします。

  • movq 命令はレジスタ %rax に 10 を転送します。
  • leaq 命令は 1000c をレジスタ %rax に転送します。

leaq 命令は単純なベースアドレスとオフセットの加算を完了します. 実際, leaq 命令はアドレスの加算を完了するだけでなく, 次の命令のような通常の算術演算でも一般的に使用されます.

leaq 7(%rdx, %rdx, 4), %rax

レジスタ %rdx の値が x であると仮定すると、この命令は %rax の値を x + 4x +7 に設定することを意味します。次のコードなど、Linux でのアドレッシング モードの計算を参照してください。

long scale(long x, long y, long z) 
{
    long t = x + 4 * y + 12 * z;
    return t;
}

/*
    long scale(long x, long y, long z)
    x in %rdi, y in %rsi, z in %rdx
*/
scale:
    leaq (%rdi,%rsi,4), %rax     x + 4*y
    leaq (%rdx,%rdx,2), %rdx     z + 2*z = 3*z
    leaq (%rax,%rdx,4), %rax     (x+4*y) + 4*(3*z) = x + 4*y + 12*z
    ret

したがって、leaq 命令は、加算および限定乗算の計算も完了することができますが、注意すべきことの 1 つは、アドレッシング モードでのスケーリング係数は 1、2、4、8 のみであるということです。1 , 2, 4, 8 とのみ比較されます. 上記のコードでは、2 行目は leaq(%rax, %rdx, 12) を使用して 1 つのステップで計算を完了することはできませんが、2 つに分割する必要があります。このための手順

2. 整数演算命令

命令 効果 説明
INC D D ← D + 1 プラス1
12月D D ← D - 1 マイナス1
いいえ D ← - D  取负
D ではない D ← ~ D  補体
追加 S、D D ← D + S 追加
サブS、D D ← D - S 減らす
イムルS、D D ← D * S 取った

これらの整数演算は、オペランドのサイズに応じてオペランド サイズ記述子と共に使用されるため、4 つの異なる命令があります。

最初の 4 つの命令 inc, dec, neg および not は、ソースと宛先の両方であるオペランドが 1 つしかないため、単項演算と呼ばれます。このオペランドは、レジスタまたはメモリ ロケーションにすることができます。

最後の 3 つの命令 add、sub、imul には 2 つのオペランドがあり、2 番目のオペランドがソースとデスティネーションの両方として使用されるため、2 項演算と呼ばれます。

2.1  INC と DEC

INC (インクリメント) 命令はオペランドから 1 を加算し、DEC (デクリメント) 命令はオペランドから 1 を減算しますが、どちらCF には影響しません。

コマンドフォーマット

  • inc reg/mem
  • 12月登録/メモリ 

inc 命令と dec 命令の動作を確認するには、以下のコードを使用してください。

#include <stdio.h>

int main() {
    int x = 10;

    // printf("The value of x before the increment: %d\n", x);    10

    __asm__ ( "inc %0\n" : "=r" (x) : "0" (x) );                   

    // printf("The value of x after the increment: %d\n", x);      11

    // printf("The value of x before the increment: %d\n", x);     11

    __asm__ ( "dec %0\n" : "=r" (x) : "0" (x) );

    // printf("The value of x after the increment: %d\n", x);      10

    return 0;
}
00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	ff c0                	inc    %eax             // 把%eax中的值加1
  4004fd:	89 45 fc             	mov    %eax,-0x4(%rbp)
  400500:	8b 45 fc             	mov    -0x4(%rbp),%eax
  400503:	ff c8                	dec    %eax             // 把%eax中的值减1
  400505:	89 45 fc             	mov    %eax,-0x4(%rbp)
  400508:	b8 00 00 00 00       	mov    $0x0,%eax
  40050d:	5d                   	pop    %rbp
  40050e:	c3                   	retq   
  40050f:	90                   	nop

2.2 マイナス 

NEG (): 数値を対応する 2 の補数に変換して、反対の数値を取得します。影響を受けるフラグは次のとおりです。

キャリフラグCF、ゼロフラグZF、符号フラグSF、オーバーフローフラグOF、補助キャリフラグAF、パリティフラグPF(結果の下位8ビットで、値1の数が偶数かどうか)。

コマンドフォーマット

  • 否定登録
  • わからない 

以下のコードを使用して、neg コマンドの動作を確認します

int main() 
{
    int i = 10;
    i = -i;
    i = ~i;

    return 0;
}
00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)
  4004f8:	f7 5d fc             	negl   -0x4(%rbp)      // i = -i;
  4004fb:	b8 00 00 00 00       	mov    $0x0,%eax
  400500:	5d                   	pop    %rbp
  400501:	c3                   	retq   
  400502:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  400509:	00 00 00 
  40050c:	0f 1f 40 00          	nopl   0x0(%rax)

2.3  ADD、SUB、IMUL

ADD (加算): 命令は、同じサイズのソース オペランドとデスティネーション オペランドを加算します。

SUB (減算): この命令は、同じサイズのソース オペランドとデスティネーション オペランドを減算します。

IMUL (乗算): 命令は、同じサイズのソース オペランドとデスティネーション オペランドを乗算します。 

コマンドフォーマット

  • ソースオペランド、デスティネーションオペランドを追加
  • サブ ソース オペランド、デスティネーション オペランド
  • imul ソース オペランド、デスティネーション オペランド

命令のソースオペランドは、即値、レジスタ、メモリ位置のいずれかです。

命令のデスティネーション オペランドは、レジスタ、メモリ ロケーションのいずれかです。

以下のコードを使用して、add、sub、および imul 命令が何を行うかを確認します

int main() 
{
    int a = 10;
    int b = a + 10;
    int c = b - 15;
    a = a * b;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)    // int a = 10;
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	83 c0 0a             	add    $0xa,%eax          // int b = a + 10;
  4004fe:	89 45 f8             	mov    %eax,-0x8(%rbp)
  400501:	8b 45 f8             	mov    -0x8(%rbp),%eax
  400504:	83 e8 0f             	sub    $0xf,%eax          // int c = b - 15;
  400507:	89 45 f4             	mov    %eax,-0xc(%rbp)
  40050a:	8b 45 fc             	mov    -0x4(%rbp),%eax
  40050d:	0f af 45 f8          	imul   -0x8(%rbp),%eax    // a = a * b;
  400511:	89 45 fc             	mov    %eax,-0x4(%rbp)
  400514:	5d                   	pop    %rbp
  400515:	c3                   	retq   
  400516:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  40051d:	00 00 00 

3.ブール命令

C言語にはビット演算子があります

以下のコマンドにそれぞれ対応 

命令 効果 説明
AND S、D D ← D & S 
またはS、D D ← D | 1 また
XOR S、D D ← D ^ S XOR
D ではない D ← ~ D  補体

3.1 かつ

 AND 命令は、オペランドの各ペアの対応するデータビット間でブール型のビット単位の "AND"演算を実行し、結果をデスティネーション オペランドに格納します。

コマンドフォーマット

  • AND reg/mem/imm、reg/mem

AND 命令は常に CF=0、OF=0 とし、デスティネーションオペランドの値に応じて SF、ZF、PF の値を変更します。 

次のコードを参照してください 

int main() 
{
    int x = 10;      // 00000000 00000000 00000000 00001010
    int y = x & 8;   // 00000000 00000000 00000000 00001000

    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)    // int x = 10;
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	83 e0 08             	and    $0x8,%eax          // int y = x & 8;
  4004fe:	89 45 f8             	mov    %eax,-0x8(%rbp)
  400501:	b8 00 00 00 00       	mov    $0x0,%eax
  400506:	5d                   	pop    %rbp
  400507:	c3                   	retq   
  400508:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
  40050f:	00 

3.2 または

OR命令は、オペランドの各ペアの対応するデータ ビット間でブールビット OR演算を実行し、結果をデスティネーション オペランドに格納します。

コマンドフォーマット

  • または reg/mem/imm、reg/mem

OR命令は常にCF=0、OF=0とし、デスティネーションオペランドの値に応じてSF、ZF、PFの値を変更します 

次のコードを参照してください 

int main() 
{
    int x = 10;      // 00000000 00000000 00000000 00001010
    int y = x | 8;   // 00000000 00000000 00000000 00001000

    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)    // int x = 10;
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	83 c8 08             	or     $0x8,%eax          // int y = x | 8
  4004fe:	89 45 f8             	mov    %eax,-0x8(%rbp)
  400501:	b8 00 00 00 00       	mov    $0x0,%eax
  400506:	5d                   	pop    %rbp
  400507:	c3                   	retq   
  400508:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
  40050f:	00 

3.3 排他的論理和

XOR命令は、オペランドの各ペアの対応するデータ ビット間でブール「排他的 OR」演算を実行し、結果をデスティネーション オペランドに格納します。 

コマンドフォーマット

  • XOR reg/mem/imm、reg/mem

OR命令は常にCF=0、OF=0とし、デスティネーションオペランドの値に応じてSF、ZF、PFの値を変更します 

次のコードを参照してください 

int main() 
{
    int x = 10;      // 00000000 00000000 00000000 00001010
    int y = x ^ 8;   // 00000000 00000000 00000000 00001000

    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)    // int x = 10;
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	83 f0 08             	xor    $0x8,%eax          // int y = x ^ 8;
  4004fe:	89 45 f8             	mov    %eax,-0x8(%rbp)
  400501:	b8 00 00 00 00       	mov    $0x0,%eax
  400506:	5d                   	pop    %rbp
  400507:	c3                   	retq   
  400508:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
  40050f:	00 

3.4 ない

NOT命令は、オペランドのすべてのデータ ビットを反転します。 

コマンドフォーマット

  • 登録/メモリではない

NOT 命令はステータス フラグを変更しません。 

次のコードを参照してください 

int main() 
{
    int x = 10;      // 00000000 00000000 00000000 00001010
    int y = ~x;      // 11111111 11111111 11111111 11110101

    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)    // int x = 10;
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	f7 d0                	not    %eax               // ~x;
  4004fd:	89 45 f8             	mov    %eax,-0x8(%rbp)    // int y = ~x;
  400500:	b8 00 00 00 00       	mov    $0x0,%eax
  400505:	5d                   	pop    %rbp
  400506:	c3                   	retq   
  400507:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
  40050e:	00 00 

4.シフト操作

C言語のシフト演算子は左シフト演算子(<<)と右シフト演算子(>>)に分けられ、シフト演算は左シフトと右シフトに分けられる

命令 効果 説明
SAL k、D D ← D << k 算術左シフト
SHLk、D D ← D << k

論理左シフト

(SAL相当)

SAR k、D D ← D >> k 算数右移
SHLk、D D ← D >> k 論理右シフト

シフト操作

  1. 第 1 オペランドはシフト量 k で、バイナリ ビットによってシフトされるビット数です。
  2. 第 2 オペランドは、シフトする数値です

注: シフト量は即値にするか、シングルバイト レジスタ %clに配置できます(ここにのみ配置できます)。 

%cl は 8 ビット長で 0 ~ 255 を表すことができるため、最大シフト量は 255 ビットに達する可能性がありますが、明らかにそのような長いデータ型は存在しないため、実際にはシフト操作は桁数に応じて決定されます。シフト. %clの値は何ですか,

x86-64 では、シフト操作は w ビット長のデータ値で動作します。シフト量は %cl レジスタの下位 m ビットによって決まります。ここでは、2 の m 乗は w に等しく、上位ビットは無視されます。 

たとえば、この時点で %cl は 0xFF です。

%cl 1111 1111

さまざまなデータ型

  • 8 ビット長の char 型のデータは、%cl で下位 3 ビット 111 を取るため、7 ビット移動します。
  • short 型のデータは 16 ビット長で、%cl の下位 4 ビットは 1111 であるため、15 ビット移動します。
  • int 型のデータは 32 ビット長で、%cl の下位 5 ビットは 11111 であるため、31 ビットが移動されます。

4.1 算術左シフトと論理左シフト

SAL (算術左シフト): 宛先オペランドに対して論理左シフト演算を実行し、下位ビットを 0 で埋め、最上位ビットを CF に送信します。

SHL(Logic  Left Shift):SAL命令相当

コマンドフォーマット

  • サル imm8/CL、reg/mem
  • shl imm8/CL、reg/mem

次のコードを参照してください 

int main() 
{
    int x = 10;      // 00000000 00000000 00000000 00001010
    int y = x << 2;  // 00000000 00000000 00000000 00101000
   
    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)     // int x = 10;
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	c1 e0 02             	shl    $0x2,%eax           // x << 2
  4004fe:	89 45 f8             	mov    %eax,-0x8(%rbp)     // int y = x << 2;
  400501:	b8 00 00 00 00       	mov    $0x0,%eax
  400506:	5d                   	pop    %rbp
  400507:	c3                   	retq   
  400508:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
  40050f:	00 

4.2 算術右シフトと論理右シフト

SHR ( Logic Shift Right  ): デスティネーション オペランドに対して論理右シフト演算を実行し、シフトされたデータ ビットは 0 で埋められ、最下位ビットが CF に送信されます。 

コマンドフォーマット 

  • shr imm8/CL、reg/mem

SAL ( Arithmetic Right Shift ): 空いたビットを最上位ビットで埋め、最下位ビットを CF にコピーする

コマンドフォーマット 

  • sar imm8/CL、reg/mem

次のコードを参照してください (ここでは、算術右シフトが最も特殊であり、算術右シフトのみが示されています)。

int main()
{
    int x1 = 10;       // 00000000 00000000 00000000 00001010
    int y1 = x1 >> 2;  // 00000000 00000000 00000000 00000010

    int x2 = -10;      // 11111111 11111111 11111111 11110110
    int y2 = x2 >> 2;  // 11111111 11111111 11111111 11111101

    return 0;
}

00000000004004ed <main>:
  4004ed:	55                   	push   %rbp
  4004ee:	48 89 e5             	mov    %rsp,%rbp
  4004f1:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)
  4004f8:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4004fb:	c1 f8 02             	sar    $0x2,%eax          // 算术右移,以0填充
  4004fe:	89 45 f8             	mov    %eax,-0x8(%rbp)
  400501:	c7 45 f4 f6 ff ff ff 	movl   $0xfffffff6,-0xc(%rbp)
  400508:	8b 45 f4             	mov    -0xc(%rbp),%eax
  40050b:	c1 f8 02             	sar    $0x2,%eax          // 算术右移,以1填充
  40050e:	89 45 f0             	mov    %eax,-0x10(%rbp)
  400511:	b8 00 00 00 00       	mov    $0x0,%eax
  400516:	5d                   	pop    %rbp
  400517:	c3                   	retq   
  400518:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
  40051f:	00 

算術右シフトは符号付き数値と符号なし数値を区別するため、算術右シフトを使用して補数を操作すると、整数演算の一部を置き換えることができます。

long arith(long x, long y, long z)
{
    long t1 = x ^ y;
    long t2 = z * 48;
    long t3 = t1 & 0x0F0F0F0F;
    long t4 = t2 - t3;
    return t4;
}

対応するコンパイルは 

/*
long arith(long x, long y, long z)
x in %rdi, y in %rsi, z in %rdx
*/

arith:
    xorq %rsi, %rdi             t1 = x ^ y
    leaq (%rdx,%rdx,2), %rax    3*z
    salq $4, %rax               t2 = 16 * (3*z) = 48*z
    andl $252645135, %edi       t3 = t1 & 0x0F0F0F0F
    subq %rdi, %rax             Return t2 - t3
    ret

ここでは、乗算の代わりに salq $4, %rax を使用すると、演算を高速化できます。

5. 特殊な算術演算 

2 つの 64 ビットの符号付きまたは符号なしの数値を乗算して得られる積を表すには、128 ビットが必要です。x86-64 命令セットは、128 ビット操作に対してある程度のサポートを提供します. Intel は、16 バイトの数値を oct ワードと呼んでいます.

次の表は、2 つの 64 ビット数と整数除算命令の完全な 128 ビット積を生成するためにサポートされています。

命令 効果 説明
イムクS R[ %rdx ]:R[ %rax ] ← S × R[ %rax ]  符号付き乗算
マルクS R[ %rdx ]:R[ %rax ] ← S × R[ %rax ] 

符号なし乗算

cqto R[ %rdx ]:R[ %rax ] ← SignExtend(R[ %rax ]) ホロスコープに変換
idivq S

R[ %rdx ] ← R[ %rdx ]:R[ %rax ]mod S

R[ %rax ] ← R[ %rdx ]:R[ %rax ]÷ S

署名された部門
divq S

R[ %rdx ] ← R[ %rdx ]:R[ %rax ]mod S

R[ %rax ] ← R[ %rdx ]:R[ %rax ]÷ S

無印の除算

2 つのレジスタ %rdx (64 ビット) と %rax (64 ビット) は 128 ビットの 8 ワードを形成し、積の上位部分が 0 かどうかに応じて CF と OF を設定またはクリアします。

符号なし乗算 (mulq) と符号付き乗算 (imulq) の場合、どちらも単一オペランドの乗算命令であり、どちらも 1 つのパラメータをレジスタ %rax に格納し、もう 1 つのパラメータを命令のソース オペランドとして格納する必要があります。レジスタ %rdx および %rax に配置

%rdx (64 ビット) %rax (64 ビット)

以下は一例です。元の CASPP 本のセクション 3.5.5 の詳細を参照してください。

#include <inttypes.h>

typedef unsigned __int128 uint128_t;

void store_uprod(uint128_t *dest, uint64_t x, uint64_t y)
{
    *dest = x * (uint128_t)y;
}
/*
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y)
dest in %rdi, x in %rsi, y in %rdx
*/

store_uprod:
    movq %rsi, %rax Copy x to multiplicand
    mulq %rdx Multiply by y
    movq %rax, (%rdi) Store lower 8 bytes at dest
    movq %rdx, 8(%rdi) Store upper 8 bytes at dest+8
    ret

おすすめ

転載: blog.csdn.net/weixin_58165485/article/details/128993909