write関数で`\0`(null文字)を書き出して| cat -eすると^@が出てくる話

こんにちは,狐と初音ミクが大好きなぐーたら狐です.
今日は^@^Mというメモリにゴミとしてよく存在する者たちの意味と,その単純な確認方なんかのお話です.

^@ってなんや

C言語で用いるwrite関数を用いると,実はnull文字が書き出せます.詳しくは次の節のwrite関数で遊ぶを参照してください.
通常は標準出力してもnull文字は表示不可能文字なので表示されてくれませんが,| cat -eを実行の際につけることで,表示不可能文字を書き出してくれます. すると^@と出力されるのです.つまりnull文字がシステムの上で^@とも表記されているらしいことが分かります.
同様に,適当なポインタを指定して,メモリ空間を100文字くらい書き出してみたりすると^Mとか^Qとかが現れたりします.この^付きの表記って意味があったりするのかなぁと思って調べてみたら,実は意味があったことが分かりました.
この表記方法はキャレット記法と呼ばれる記法です.
^はctrlキーを意味します.Mはasciiコード上で10進数として77ですが,^はこのasciiコードから10進数で-64するという操作を意味します.つまり,^Mはasciiコードとして10進数で13を意味します.ここでascii コード表を見てみると13には 'CR'が充てられていることが分かります.これは改行コードの一種です.
同様に^@について考えてみると@はasciiコード上で10進数として64のため,'^@'はasciiコード上で0を指すとわかります.これはnull文字です.
このようにして,キャレット記法は,asciiコード上の表示不可能文字をshell上で入力するために用いられる記法になります.
よく無限ループに陥ったとき,Ctrl + Cを押しますよね?これはつまり^Cなわけですが,これのasciiコードは3であり,このコードはETX(テキスト終了)を意味します.
標準入力の終了はCtrl + Dですが,これは^Dはasciiコードで4であり,これはEOT(転送終了)を意味します.そういうものだと思っていた入力にもちゃんと意味があるんですね...

write関数で遊ぶ

まず,write関数について簡単に説明をします.
write関数はC言語において出力用の関数として用意されている標準関数の一つです.正確には,システムコールをラップしてる関数ですね.unistd.hヘッダーにおいて定義されています.
プロトタイプ宣言は次のようになります.

write(int fd, const void *buf, size_t count);

write関数はfdで指定されるファイルディスクリプタに対応するファイル(fd = 0,1,2の場合は特殊)に,bufで指定されたポインタを先頭とする文字列をcount分出力してくれます.
fd=0の時は標準入力,fd=1の時は標準出力,fd=2の時は標準error出力となっています.
このwrite関数はC言語でよく使われるprintfと違う部分として,null文字があろうとなかろうとcountで指定したバイト数分,文字を書き出してくれるという点があります.
なので,write関数を用いると以下のようなことができます.

#include <unistd.h>
#include <stdlib.h>

int main(){
    char *s;
    
    s = calloc(sizeof(char), 10);
    write(1, s, 10);
}

callocという関数は,第一引数と第二引数の掛け算byte分mallocして取った領域を全てnull埋めしてくれる関数です.
なのでこのコードでは,先頭から10文字分はnull文字が入っているようなメモリ領域をwriteで10文字分書き出すコードとなります.
gccコンパイルして,単にa.outを実行すると,たいていの場合には何も出力はされません.
これは,null文字が表示不可能文字だからです.
しかし,例えばここで./a.out | cat -eを実行してみましょう.catの-eオプションは渡された文字に表示不可能文字があっても出力するオプションです.その結果として以下のような結果を得ます.

>^@^@^@^@^@^@^@^@^@^@

つまり,null文字が詰まっている部分に10個^@が並んでいることが分かります.
これは,null文字が^@で表されていることの簡単な確認となるでしょう.
しかし,^Mのようなコードのasciiが13であることはどうやったら確認できるでしょうか?これも正直単純で以下のようにするといいでしょう.

#include <stdlib.h>

int main(){
    char s[1];
    
    s[0] = 13;
    write(1, s, 1);
}

上記のコードを書き,./a.out | cat -eで先ほどのように表示してみましょう.きっと標準出力として^Mを得るでしょう. ちなみに,./a.out | xxd -g 1とかするとさらに見やすくなるでしょう.xxdコマンドは以下のように出力を与えてくれるダンプ出力のコマンドです.-gオプションの後ろにつける数字に対応したバイト数でグルーピングしてくれます.

$ echo "abcdefghijklmnopqrstu" | xxd -g 1
0000000000: 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 abcdefghijklmnop
0000000010: 71 72 73 74 75                                                      qrstu

これを使って,上のnull文字列とかを表示してやると,今度はasciiコードの確認ができます.
他にもodコマンドとか,hexdumpとかもメモリの書き出しには便利ですね.