LoginSignup
3
0

More than 5 years have passed since last update.

Cの文字列関数のインラインアセンブラの解読

Posted at

文字列処理系の関数のインラインアセンブラの解読

参考にしたところ

環境はLinux、使ったアーキテクチャはx86, x64です。

生成方法

インラインの命令として生成してるのはGCCっぽいのでglibcのソースを見ても出てきませんでした。
特定の条件のときだけだったり、最適化をかけないと出てこないものもある。
今回対象にする命令はstrlen, strcpy, strcmpの3つ。徐々に増やしたい。

strlen

まずはサンプルコード。

#include <stdio.h>
#include <string.h>

int main() {
  char buf[1024], buf_cpy[1024];

  fgets(buf, 512, stdin);

  int len = strlen(buf);
  printf("%d\n", len);

  return 0;
}

表示しているのは最適化による削除対策なので無視してください。
コンパイルはgcc -m32 -minline-all-stringops -O1 sample.c
-minline-all-stringopsは文字列関数でインラインアセンブリコードを使うように指示するオプションになる。
とりあえずx86。

strlenに対応している部分のアセンブリコードは以下のような感じ。
逆アセンブルにはradare2を使った。

0x000011e9      8dbde4fbffff   lea edi, dword [s]
0x000011ef      57             push edi                    ; char *s
0x000011f0      e84bfeffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
;; 実際のstrlenの処理はここから
0x000011f5      b9ffffffff     mov ecx, 0xffffffff         ; -1
0x000011fa      b800000000     mov eax, 0
0x000011ff      f2ae           repne scasb al, byte es:[edi]
0x00001201      89c8           mov eax, ecx
0x00001203      f7d0           not eax
0x00001205      83e801         sub eax, 1
;; eaxが返り値、文字列の長さになってる。

[https://hp.vector.co.jp/authors/VA014520/asmhsp/chap6.html]
ここ見た方が早いけど、どうせなので自分用に。

最初のcall sym.imp.fgetsでediに格納されたアドレスに文字列が入っている点に注意。
重要なのはrepne scasbとなっているところ。
repneは後述のストリング処理命令と組み合わせて、ある処理を繰り返す処理を実行できる。
他のものも含めて、rep, repe/repz, repne/repnzの3種類がある。
基本的にはecxに指定した回数だけ繰り返すが、repeとrepneはZFも見る。

一方scasbはストリング処理命令というものの一種で、repなどの繰り返しと組み合せてあるアドレスから順番に値を処理していくのに使う。
ins, outs, movs, lods, stos, cmps, scasなどがある。testsみたいなのもあるらしいが、使わないし飛ばす。
ins/outsはポートのI/Oに対応する命令になる。
movs, lods, stosはデータ転送の命令だけど、アドレスへ読み込むのかレジスタへ読み込むのかの違いがある。
cmps, scasはそれぞれ値の比較をしてEFLAGSをセットする。メモリの値と比較するかレジスタと比較するかの違いがある。
この命令に、処理する値の幅を指定するb(byte 8bit), w(word 16bit), d(double word 32bit)が付く。
scasbだのscasのバイト単位での処理という意味になる。

ざっくり説明するとこんな感じです。
ではstrlenで使われている命令を見てみよう。
使われているのはrepne scasb al, byte es:[edi]で、ecxには-1(0xffffffff)が指定されている。
ecxの回数は符号なしとして解釈されるので、そのままデクリメントされていくようになってる。
scasbはバイト単位でscasを実行する。scasはレジスタalの値と指定されたediのアドレスが指すメモリ中の値を比較する。
ediは自動的にインクリメントされる。DFフラグによってデクリメントになるらしい。
eaxは0になっているのでalも当然0、つまりヌルバイトが出現するかどうかを延々と比較する。
ここでrepneが使われているが、これはZF=1になったらループを終了するようになっている。
つまり、比較している値が0になったらループが終了するようになっている。
まとめると、repne scasb al, byte es:[edi]ではediが指す入力した文字列中にヌルバイトが見つかるまで比較しつづけるというような処理をする。
次に、mov eax, ecx; not eax; sub eax, 1;という処理を行っている。
ecxには0xffffffffからヌルバイトまでの文字数(ヌルバイトを含む)を引いた値が入っているので、notを取って1を引くと2の補数的に文字数になる。
ヌルバイト分が回数が増えているので1引く必要がある点に注意。

とまあstrlenはこんな処理になってる。

strcpy

サンプルコード

#include <stdio.h>
#include <string.h>
#define SIZE 32

int main() {
  char buf_cpy[SIZE];
  char *buf = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";  

  strcpy(buf_cpy, buf);
  printf("%s\n", buf_cpy);

  return 0;
}

コンパイルはgcc -m32 -minline-all-stringops -O1 sample.c

アセンブリコード

0x000011c9      8d45c4         lea eax, dword [s]
0x000011cc      8db308e0ffff   lea esi, dword [ebx - 0x1ff8]
0x000011d2      b910000000     mov ecx, 0x10
0x000011d7      89c7           mov edi, eax
0x000011d9      f3a5           rep movsd dword es:[edi], dword ptr [esi]
0x000011db      0fb616         movzx edx, byte [esi]
0x000011de      8817           mov byte [edi], dl

コンパイル時に文字列の長さを計算して、ecxに0x10を格納しています。4バイト単位でコピーするため、長さが微妙に違う点に注意。
使っているのはmovsdで、4バイト単位で値を移します。これはおそらく高速化のために一度に4バイトコピーしているんじゃないかと思われます。
その分配列のために確保されている領域オーバーする可能性があるので、コンパイルすると警告が出るようになってます。
ediにはコピー先のアドレスが、esiにはコピー元のアドレスが指定されます。
この例だとesiの指すアドレスからediの指すアドレスへのコピーが行われ、それぞれがインクリメントされます。
それがecx回、つまり文字列の長さだけ行われ文字列のコピーが完了します。

strcmp

サンプルコード

#include <stdio.h>
#include <string.h>
#define SIZE 32

int main() {
  char *buf = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  char buf2[SIZE];

  fgets(buf2, SIZE, stdin);

  if (strcmp(buf, buf2)) {
    printf("%s\n", buf2);
  }

  return 0;
}

コンパイルはgcc -m32 -minline-all-stringops -O1 sample.c

アセンブリコード

0x000011e1      8d7dc4         lea edi, dword [s]
0x000011e4      57             push edi                    ; char *s
0x000011e5      e846feffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
0x000011ea      b941000000     mov ecx, 0x41               ; 'A'
0x000011ef      8db308e0ffff   lea esi, dword [ebx - 0x1ff8]
0x000011f5      f3a6           repe cmpsb byte [esi], byte ptr es:[edi] ; [0x170000001c:1]=255 ; 98784247836
0x000011f7      0f97c0         seta al
0x000011fa      1c00           sbb al, 0
0x000011fc      83c410         add esp, 0x10
0x000011ff      84c0           test al, al

mov ecx, 0x41でecxに入る0x41は文字列の長さです。
esiには比較する文字列のアドレスが、ediにはfgetsで取得した文字列のアドレスがそれぞれ入っています。
cmpsbで1バイト単位でesiとediのから取り出した値を比較している。
repeでZF=0になったらループを終了するようになっているので、比較している値が異なっているとその時点でループを抜けます。
setaはEFLAGSにもとづいて0/1を設定する命令で、al = (CF == 0 && ZF == 0)という感じの処理をします。
その次のsbb al, 0はCFを考慮した減算になっていて、減算をしたあとさらにCF分減算します。
つまり、CFがあるとalが0xffになり次のtestの判定で使われることになる。この辺はよく分からない。


とりあえずここまでにする。
他の関数も追加していきたい。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0