memset-闇の側





「C / C ++の世界で最も危険な機能」という記事を読んだ後、暗いmemsetセラーに潜む悪について深く掘り下げ、問題の本質をより広く明らかにするための補遺を書くことが有用だとわかりました。



C言語では、memset()があらゆる場所で使用され、多くのトラップがあります。 C ++リファレンスからの抜粋:

void * memset(void * ptr、int value、size_t num);

メモリのブロックを埋める

ptrが指すメモリブロックの最初のnumバイトを、指定された値(符号なしcharとして解釈される)に設定します。

パラメータ

ptr-埋めるメモリブロックへのポインタ。

value-設定する値。 値はintとして渡されますが、関数はこの値のunsigned char変換を使用してメモリブロックを埋めます。

num-値に設定されるバイト数。 size_tは、符号なし整数型です。

戻り値

ptrが返されます。


繰り返し指摘されているように、経験豊富な開発者でさえ踏み込んだ多くのレーキがあります。 Andrey2008で説明されている典型的なエラーの簡単な要約から:



1番 配列または構造のサイズを計算しようとすると、配列/構造へのポインタにsizeof()を使用しないでください。配列/構造のサイズの代わりに4または8バイトのポインタサイズが返されます。



2番 memset()の3番目の引数は、データ型に関係なく、要素の数ではなくバイト数を入力として受け入れます。 また、たとえば、int型は、アーキテクチャに応じて4バイトまたは8バイトを占有できることも追加します。 この場合、sizeof(int)を使用します。



番号3。 引数を混同しないでください。 正しいシーケンスは、ポインター、値、バイト単位の長さです。



番号4。 クラスオブジェクトを操作するときは、memsetを使用しないでください。



しかし、これは氷山の一角にすぎません。



代替セット



memsetは、開発者がコンピューターアーキテクチャのすべての機能を考慮する必要がある低レベルの関数であり、その使用は正当化される必要があります。 memsetの代わりにalternative = {0}を検討することから始めましょう。これにより、実行時にデータを初期化するmemset(ZeroMemory)とは異なり、コンパイル段階で配列または文字列を初期化でき、プログラムの速度が向上します。 私はそれをチェックアウトすることにしました。



void doInitialize() { char p0[25] = {0} ; //   25   0 char p1[25] = "" ; //   25   0 wchar_t p2[25] = {0} ; //  25   0 wchar_t p3[25] = L"" ; //   25   0 short p4[62] = {0} //  62   0 int p5[37] = {-1} ; //      -1 unsigned int p6[10] = {89} ; //     89 }
      
      





C99 [$ 6.7.8 / 21]

ブレースで囲まれたリストの初期化子が集合体の要素またはメンバーより少ない場合、または既知のサイズの配列を初期化するために使用される文字列リテラルの文字が配列内の要素より少ない場合、集合体の残りは静的ストレージ期間を持つオブジェクトと同じように暗黙的に初期化されます。


同時に、この初期化は、パラメータとバッファサイズの混乱を伴う問題1、2、3を取り除きます。 つまり、場所によっては2番目と3番目の引数を混同しないため、サイズを転送する必要はありません。 コンパイラーがそのようなコードを変換する方法を見てみましょう。 すべてのコンパイラをすぐに確認することはできません。android-ndk-r10cに含まれるgccとubuntu 14.04に含まれるgccが手元にあることがわかりました。



gcc -v
1)gccバージョン4.9 20140827(プレリリース)(GCC)

2)gccバージョン4.8.2(Ubuntu 4.8.2-19ubuntu1)



このようなコードでコンパイラがどのように動作するかを見てみましょう。



 void empty_string(){ int i; char p1[25] = {0}; printf("\np1: "); for (i = 0; i < 25; i++) printf("%x,",p1[i]); }
      
      





そのため、最適化(-O0)なしで、配列の初期化はそのようなアセンブラコードにコンパイルされます(objdumpを使用してバイナリを調べます)。



gcc -O0、ELF 32ビット、ARM、EABI5
  83d8: e3a03000 mov r3, #0 83dc: e50b3024 str r3, [fp, #-36] ; 0x24 83e0: e24b3020 sub r3, fp, #32 83e4: e3a02000 mov r2, #0 83e8: e5832000 str r2, [r3] 83ec: e2833004 add r3, r3, #4 83f0: e3a02000 mov r2, #0 83f4: e5832000 str r2, [r3] 83f8: e2833004 add r3, r3, #4 83fc: e3a02000 mov r2, #0 8400: e5832000 str r2, [r3] 8404: e2833004 add r3, r3, #4 8408: e3a02000 mov r2, #0 840c: e5832000 str r2, [r3] 8410: e2833004 add r3, r3, #4 8414: e3a02000 mov r2, #0 8418: e5832000 str r2, [r3] 841c: e2833004 add r3, r3, #4 8420: e3a02000 mov r2, #0 8424: e5c32000 strb r2, [r3] 8428: e2833001 add r3, r3, #1
      
      







gcc -O0、ELF 64ビット、x86-64
  400700: 48 c7 45 d0 00 00 00 00 movq $0x0,-0x30(%rbp) 400708: 48 c7 45 d8 00 00 00 00 movq $0x0,-0x28(%rbp) 400710: 48 c7 45 e0 00 00 00 00 movq $0x0,-0x20(%rbp) 400718: c6 45 e8 00 movb $0x0,-0x18(%rbp)
      
      







予想どおり、最適化なしで、O(n)プロセッサ時間(nはバッファの長さ)を消費する実行時コードを取得します。 コンパイラーが最適化(-O3)を行うと、次のようになります。



gcc -O3、32ビット、ARM



 000083ac <empty_string>: 83ac: e59f002c ldr r0, [pc, #44] ; 83e0 <empty_string+0x34> 83b0: e92d4038 push {r3, r4, r5, lr} 83b4: e08f0000 add r0, pc, r0 83b8: ebffffb2 bl 8288 <printf@plt> 83bc: e59f5020 ldr r5, [pc, #32] ; 83e4 <empty_string+0x38> 83c0: e3a04019 mov r4, #25 83c4: e08f5005 add r5, pc, r5 83c8: e1a00005 mov r0, r5 83cc: e3a01000 mov r1, #0 83d0: ebffffac bl 8288 <printf@plt> 83d4: e2544001 subs r4, r4, #1 83d8: 1afffffa bne 83c8 <empty_string+0x1c> 83dc: e8bd8038 pop {r3, r4, r5, pc}
      
      



gcc -O3、64ビット、x86-64
 00000000004006d0 <empty_string>: 4006d0: 53 push %rbx 4006d1: be a4 08 40 00 mov $0x4008a4,%esi 4006d6: bf 01 00 00 00 mov $0x1,%edi 4006db: 31 c0 xor %eax,%eax 4006dd: bb 32 00 00 00 mov $0x32,%ebx 4006e2: e8 d9 fd ff ff callq 4004c0 <__printf_chk@plt> 4006e7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 4006ee: 00 00 4006f0: 31 d2 xor %edx,%edx 4006f2: 31 c0 xor %eax,%eax 4006f4: be aa 08 40 00 mov $0x4008aa,%esi 4006f9: bf 01 00 00 00 mov $0x1,%edi 4006fe: e8 bd fd ff ff callq 4004c0 <__printf_chk@plt> 400703: 83 eb 01 sub $0x1,%ebx 400706: 75 e8 jne 4006f0 <empty_string+0x20> 400708: 5b pop %rbx 400709: c3 retq
      
      







実行時にゼロが設定されたコードが消えたことがわかり、約束されたO(1)のパフォーマンスが得られました。printfが値を取得する場所を把握しましょう。 この作品に興味があります:



 83bc: ldr r5, [pc, #32] 83c0: mov r4, #25 ;//  r4    for,     83c4: add r5, pc, r5 ;//  r5   "%x,"  ,      002c7825 83c8: mov r0, r5 ;// r5    r0    ,    printf() 83cc: mov r1, #0 ;//   0 (  p1[i])    printf() 83d0: bl 8288 <printf@plt> 83d4: subs r4, r4, #1 ;//      83d8: bne 83c8 <empty_string+0x1c> ;//     0,      83c8
      
      





つまり、コンパイラは単に配列を破棄し、その値の代わりに、コンパイル段階で設定された定数として0を使用します。 わかりましたが、 memsetを使用するとどうなりますか? たとえば、ARMの下でいくつかのobjdumpを見てみましょう。



最適化なし-O0



  83d8: e24b3024 sub r3, fp, #36 ; 0x24 83dc: e1a00003 mov r0, r3 83e0: e3a01000 mov r1, #0 83e4: e3a02019 mov r2, #25 83e8: ebffffa3 bl 827c <memset@plt>
      
      





最適化-O3を使用



  83c0: e58d3004 str r3, [sp, #4] 83c4: e58d3008 str r3, [sp, #8] 83c8: e58d300c str r3, [sp, #12] 83cc: e58d3010 str r3, [sp, #16] 83d0: e58d3014 str r3, [sp, #20] 83d4: e58d3018 str r3, [sp, #24] 83d8: e5cd301c strb r3, [sp, #28]
      
      





x86-64
最適化なし-O0:

  400816: ba 19 00 00 00 mov $0x19,%edx 40081b: be 00 00 00 00 mov $0x0,%esi 400820: 48 89 c7 mov %rax,%rdi 400823: e8 a8 fc ff ff callq 4004d0 <memset@plt>
      
      





最適化-O3の場合:

  4007f4: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp) 4007fc: 48 c7 44 24 08 00 00 00 movq $0x0,0x8(%rsp) 400805: 48 c7 44 24 10 00 00 00 movq $0x0,0x10(%rsp) 40080e: c6 44 24 18 00 movb $0x0,0x18(%rsp)
      
      







つまり、最適化は、インラインで挿入することでmemset呼び出しを単に削除します。 そのような場合、 memsetは常にO(n)時間動作しますが、最適化中の= {0}での初期化は定数に対して動作します。ゼロ。 しかし、これが常に当てはまるのか、初期化後にゼロ以外の値を書き込むとどうなるのかを見てみましょう。 テスト関数は次のようになります。



 void empty_string(){ int i; char p1[25] = {0}; p1[0] = 65; printf("\np1: "); for (i = 0; i < 25; i++) printf("%x,",p1[i]); }
      
      





コンパイル後、すでにおなじみのコードブロックを取得します。



  8404: e3a02041 mov r2, #65 ; 0x41 8408: e08f0000 add r0, pc, r0 840c: e58d3004 str r3, [sp, #4] 8410: e58d3008 str r3, [sp, #8] 8414: e58d300c str r3, [sp, #12] 8418: e58d3010 str r3, [sp, #16] 841c: e58d3014 str r3, [sp, #20] 8420: e58d3018 str r3, [sp, #24] 8424: e5cd301c strb r3, [sp, #28] 8428: e5cd2004 strb r2, [sp, #4]
      
      





x86-64
  4006f8: 48 c7 04 24 00 00 00 movq $0x0,(%rsp) 4006ff: 00 400700: 48 c7 44 24 08 00 00 movq $0x0,0x8(%rsp) 400707: 00 00 400709: 48 c7 44 24 10 00 00 movq $0x0,0x10(%rsp) 400710: 00 00 400712: c6 44 24 18 00 movb $0x0,0x18(%rsp) 400717: c6 04 24 41 movb $0x41,(%rsp)
      
      







そして、コンパイラが最適化されたバージョンのmemsetを挿入したように見えます。 そして、配列のサイズが大幅に増加した場合に何が起こるか見てみましょう。 25バイトではなく25キロバイトと言ってください!



  83fc: e24ddc61 sub sp, sp, #24832 ; 0x6100 8400: e24dd0a8 sub sp, sp, #168 ; 0xa8 8404: e3a01000 mov r1, #0 8408: e59f2054 ldr r2, [pc, #84] ; 8464 <empty_string+0x6c> 840c: e1a0000d mov r0, sp 8410: ebffff99 bl 827c <memset@plt>
      
      





x86-64
  400720: 55 push %rbp 400721: ba a8 61 00 00 mov $0x61a8,%edx 400726: 31 f6 xor %esi,%esi 400728: 53 push %rbx 400729: 48 81 ec b8 61 00 00 sub $0x61b8,%rsp 400730: 48 89 e7 mov %rsp,%rdi 400733: 48 8d ac 24 a8 61 00 lea 0x61a8(%rsp),%rbp 40073a: 00 40073b: 48 89 e3 mov %rsp,%rbx 40073e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 400745: 00 00 400747: 48 89 84 24 a8 61 00 mov %rax,0x61a8(%rsp) 40074e: 00 40074f: 31 c0 xor %eax,%eax 400751: e8 8a fd ff ff callq 4004e0 <memset@plt> 400756: be 54 09 40 00 mov $0x400954,%esi 40075b: bf 01 00 00 00 mov $0x1,%edi 400760: 31 c0 xor %eax,%eax 400762: c6 04 24 41 movb $0x41,(%rsp) 400766: e8 a5 fd ff ff callq 400510 <__printf_chk@plt> 40076b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 400770: 0f be 13 movsbl (%rbx),%edx 400773: 31 c0 xor %eax,%eax 400775: be 5a 09 40 00 mov $0x40095a,%esi 40077a: bf 01 00 00 00 mov $0x1,%edi 40077f: 48 83 c3 01 add $0x1,%rbx 400783: e8 88 fd ff ff callq 400510 <__printf_chk@plt> 400788: 48 39 eb cmp %rbp,%rbx 40078b: 75 e3 jne 400770 <empty1_string+0x50> 40078d: 48 8b 84 24 a8 61 00 mov 0x61a8(%rsp),%rax 400794: 00 400795: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 40079c: 00 00 40079e: 75 0a jne 4007aa <empty1_string+0x8a> 4007a0: 48 81 c4 b8 61 00 00 add $0x61b8,%rsp 4007a7: 5b pop %rbx 4007a8: 5d pop %rbp 4007a9: c3 retq
      
      







わあ!



行= {0}は闇の側に行き、memsetは喜ぶ!



ただし、パラメータに関する問題を解決することを決定したにもかかわらず、忘れることはありません。引数を混同することに成功しません。



ライン初期化



array = ""を初期化するオプションを考慮することも不必要ではありません。 Cはゼロで終わる文字列を使用します。つまり、バイト値が0x00の最初の文字は文字列の終わりを意味します。 したがって、行を初期化するために、すべての要素を無効にすることは意味がなく、最初の要素をリセットするだけで十分です。 空の文字列を初期化する方法は次のとおりです。



 void doInitializeCString() { char p0[25] = {0} ; //     0 char p1[25] = "" ; //     0 char p2[25] ; p2[0] = 0 ; //     0 char p3[25] ; memset(p3, 0, sizeof(p3)) ; //  25   0 char p4[25] ; strcpy(p4, "") ; //     0 char *p5 = (char *) calloc(25, sizeof(char)) ; //     0 }
      
      





初期化が= ""を介して最も信頼できる方法 、objdumpを再度解析することです。 最適化がなければ、特別なものは表示されず、すべてが{0}に似ているため、すぐに-O3オプションで検討します。 したがって、ARMでコンパイルします。

ここに関数があります
 void empty_string(){ int i; char p1[25] = ""; printf("\np1: "); for (i = 0; i < 25; i++) printf("%x,",p1[i]); }
      
      







そして、突然、配列のすべての要素がゼロになります。



  83c0: e58d3004 str r3, [sp, #4] 83c4: e58d3008 str r3, [sp, #8] 83c8: e58d300c str r3, [sp, #12] 83cc: e58d3010 str r3, [sp, #16] 83d0: e58d3014 str r3, [sp, #20] 83d4: e58d3018 str r3, [sp, #24] 83d8: e5cd301c strb r3, [sp, #28]
      
      





x86-64
  400768: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp) 400770: 48 c7 44 24 08 00 00 00 movq $0x0,0x8(%rsp) 400779: 48 c7 44 24 10 00 00 00 movq $0x0,0x10(%rsp) 400782: c6 44 24 18 00 movb $0x0,0x18(%rsp)
      
      







まあ! ヌルで終わる文字列の未使用文字をすべて無効にするのはなぜですか? 1バイトをリセットするだけで十分です。 うーん、そして25000バイトがある場合、それは何をしますか? そしてここに何があります:



  8474: e24ddc61 sub sp, sp, #24832 ; 0x6100 8478: e24dd0a8 sub sp, sp, #168 ; 0xa8 847c: e3a0c000 mov ip, #0 8480: e28d3f6a add r3, sp, #424 ; 0x1a8 8484: e1a0100c mov r1, ip 8488: e59f204c ldr r2, [pc, #76] ; 84dc <empty_string+0x6c> 848c: e28d0004 add r0, sp, #4 8490: e503c1a8 str ip, [r3, #-424] ; 0x1a8 8494: ebffff78 bl 827c <memset@plt>
      
      





x86-64
 00000000004007b0 <empty_string>: 4007b0: 55 push %rbp 4007b1: ba a0 61 00 00 mov $0x61a0,%edx 4007b6: 31 f6 xor %esi,%esi 4007b8: 53 push %rbx 4007b9: 48 81 ec b8 61 00 00 sub $0x61b8,%rsp 4007c0: 48 8d 7c 24 08 lea 0x8(%rsp),%rdi 4007c5: 48 8d ac 24 a8 61 00 lea 0x61a8(%rsp),%rbp 4007cc: 00 4007cd: 48 c7 04 24 00 00 00 movq $0x0,(%rsp) 4007d4: 00 4007d5: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4007dc: 00 00 4007de: 48 89 84 24 a8 61 00 mov %rax,0x61a8(%rsp) 4007e5: 00 4007e6: 31 c0 xor %eax,%eax 4007e8: 48 89 e3 mov %rsp,%rbx 4007eb: e8 f0 fc ff ff callq 4004e0 <memset@plt> 4007f0: be 54 09 40 00 mov $0x400954,%esi 4007f5: bf 01 00 00 00 mov $0x1,%edi 4007fa: 31 c0 xor %eax,%eax 4007fc: e8 0f fd ff ff callq 400510 <__printf_chk@plt> 400801: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 400808: 0f be 13 movsbl (%rbx),%edx 40080b: 31 c0 xor %eax,%eax 40080d: be 5a 09 40 00 mov $0x40095a,%esi 400812: bf 01 00 00 00 mov $0x1,%edi 400817: 48 83 c3 01 add $0x1,%rbx 40081b: e8 f0 fc ff ff callq 400510 <__printf_chk@plt> 400820: 48 39 eb cmp %rbp,%rbx 400823: 75 e3 jne 400808 <empty_string+0x58> 400825: 48 8b 84 24 a8 61 00 mov 0x61a8(%rsp),%rax 40082c: 00 40082d: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 400834: 00 00 400836: 75 0a jne 400842 <empty_string+0x92> 400838: 48 81 c4 b8 61 00 00 add $0x61b8,%rsp 40083f: 5b pop %rbx 400840: 5d pop %rbp 400841: c3 retq
      
      







暗いmemsetが私たちを追いかけているようです。 あなたがまだ闇と戦うことを望むならば、他のotherがあなたを待っていることを言及する価値があります。







memsetは数値を誤った値で初期化する場合があります



整数の配列にゼロ以外の値を入力する場合は、バイト入力データを確認してください。



 void doInitializeToMistakenValues() { char pChar[25] ; unsigned char pUChar[25] ; short pShort[25] ; unsigned short pUShort[25] ; int pInt[25] ; unsigned int pUInt[25] ; //  2-  4-      memset(pChar, 1, sizeof(pChar)) ; // 1 memset(pUChar, 1, sizeof(pUChar)) ; // 1 memset(pShort, 1, sizeof(pShort)) ; // 257 memset(pUShort, 1, sizeof(pUShort)) ; // 257 memset(pInt, 1, sizeof(pInt)) ; // 16843009 memset(pUInt, 1, sizeof(pUInt)) ; // 16843009 //  unsigned    0xFF memset(pChar, -1, sizeof(pChar)) ; // -1 memset(pUChar, -1, sizeof(pUChar)) ; // 255 memset(pShort, -1, sizeof(pShort)) ; // -1 memset(pUShort, -1, sizeof(pUShort)) ; // 65535 memset(pInt, -1, sizeof(pInt)) ; // -1 memset(pUInt, -1, sizeof(pUInt)) ; // 4294967295 }
      
      





それがどうなるか見てみましょう。 さて、int配列があり、2番目のパラメーターをユニットに渡したとしましょう。どうなりますか?



そしてここに何があります:



0x01010101-16進表記では、各バイトが1で埋められ、正しい値になります

0x00000001は、memset関数では設定できません。 しかし、実際にはこれはバグではなく、機能です。



それはこれらの機能の単なる無知であり、予測不可能なエラーにつながります。



memsetが無効な値を設定する場合があります



バイト-1を2つの要素に設定すると、値Not-A-Number(NaN)が得られ、その後の計算の後、値NaNを持つ各操作はNaNを返すため、計算チェーン全体が中断されます。



同様に、ブール型に-1を設定するのは正しくなく、正式にはtrueまたはfalseにはなりません。 ほとんどの場合、それはtrueとして動作します。 ほとんどの場合...



最後に、memsetは単純なデータ構造でのみ動作するように設計されています 。 マネージデータ構造でmemsetを使用しないでください。この関数は低レベルの操作のみを目的としています。





材料memsetが使用する記事は悪です。



printf関数の脆弱性についてもお読みください



All Articles