hiro99ma blog

何か技術的なこと

年末 (3)

2024/12/28

はじめに

年末なので C言語で昔作ったプログラムの改修をすることにした。
来年になったら勉強を兼ねて一から作り直したいと思うが、その気力があるかどうかわからん。

コードを公開しないので、作業中に出てきた問題や理解していなかったことをメモがてら記録していく。
全然まとまりはない。

RIPEMD160 の結果とAI

libssl を使うのを止めて libtomcrypt に変更した。
一発で SHA256 や RIPEMD160 を計算する関数は無いのだが、まあ一発で書けるやつも中で同じことをしているだけだろう。

作ったらテストだ。
ただ RIPEMD160 ってそこまでメジャーではない(個人の感想です)し、WSL2 にインストールされている OpenSSL では計算できない。

$ echo -n "abc" | openssl dgst -ripemd160
Error setting digest
40F7B41D6E7F0000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:../crypto/evp/evp_fetch.c:349:Global default library context, Algorithm (RIPEMD160 : 99), Properties ()
40F7B41D6E7F0000:error:03000086:digital envelope routines:evp_md_init_internal:initialization error:../crypto/evp/digest.c:254:

あとで調べるとここら辺にデータがあることに気付いたのだが、そのときは調べてなかった。

いろいろ ChatGPT 氏に訊いていたところだったので、テストデータも作ってくれないかと依頼したら出してくれた。

RIPEMD-160 テストデータ

    空文字列
        入力: ""(空文字列)
        ハッシュ値: 9c1185a5c5e9fc54612808977ee8f548b2258d31

    英小文字のアルファベット
        入力: "a"
        ハッシュ値: 0bdc9d2d256b3ee9daae347be6f4dc835a467ffe

    英小文字のアルファベット(複数文字)
        入力: "abc"
        ハッシュ値: 8eb208f7e05d987a9b04c27de5f74d4edfc789d4

    短文
        入力: "message digest"
        ハッシュ値: 5d0689ef49d2fae572b881b123a85ffa21595f36

    長い英単語の繰り返し
        入力: "abcdefghijklmnopqrstuvwxyz"
        ハッシュ値: f71c27109c692c1b56bbdceb5b9d2865b3708dbc

    英数字の組み合わせ
        入力: "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
        ハッシュ値: 12a053384a9c0c88e405a06c27dcf49ada62eb2b

    英数字の完全セット
        入力: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
        ハッシュ値: b0e20b6e3116640286ed3a87a5713079b21f5189

    特定文字の長い繰り返し
        入力: 8文字の"1234567890"を80回繰り返したもの(合計800文字)
        ハッシュ値: 9b752e45573d4b39f4dbd3323cab82bf63326bfb

おおありがたい!と、空文字列と “abc” を組み込んでテストした。
が、空文字列は一致したものの “abc” の方はあわない。 ハッシュ値なので全部一致しないならわかるが、前半の半分くらいは一致している。

このときは libssl を使ったサンプルコードもセットだったので「テストケースの n 番が間違ってる」と ChatGPT 氏に指摘した。
が・・・

Test case 3 の値が正しくない

ご指摘ありがとうございます!確かに、Test Case 3(入力が"abc"の場合)の正しいRIPEMD-160のハッシュ値は以下のとおりです:

Test case 3 (入力: "abc")
正しいハッシュ値: 8eb208f7e05d987a9b04c27de5f74d4edfc789d4

違う、違うよ!

値まで付けて指摘した。

“abc”をripemd160した結果は8eb208f7e05d987a9b044a8e98c6b087f15a0bfcではないか?

RIPEMD-160の結果について再度検証しました。**"abc"**を入力にしたRIPEMD-160の正しい結果は以下の通りです:

8eb208f7e05d987a9b044a8e98c6b087f15a0bfc

書き直しはしてくれたのだが、なかなか油断ならんな。
“4o” さんだったからだろうか?
また、私が忘れた頃にまた同じ質問をしたらどうなるんだろうか。
気になるが、忘れたら質問もしないだろうと思う。

いや、今教えた結果も間違っていたと教えたらどうなるだろう。

“abc”をripem160した結果は8eb208f7e05d987a9b04c27de5f74d4edfc789d4ではないか?

"abc"をRIPEMD-160ハッシュで計算した結果について再度確認しました。
正しい計算結果

入力が "abc" の場合、RIPEMD-160の正しいハッシュ値は以下の通りです:

8eb208f7e05d987a9b044a8e98c6b087f15a0bfc

これが公式のRIPEMD-160ハッシュテストケースおよび標準ツールの結果とも一致します。

まあ、わかってくれたんならいいけどさ。

typedef int INT;

いい機会なので IPA さんの コーディング作法ガイド[C言語版]ESCR Ver.3.0 を眺めていた。
いろいろ実例があって、その中に typedef int INT; があった。

そうそう、そうなんだよ。
C言語の int は CPU が整数型を扱うのに普通なやつ、みたいな扱いなので、ビルドする環境によって値の範囲が違うのだ。
32bit くらいになると結構な範囲の値が扱えるのだが、8bit や 16bit だと微妙だ。
8bit だと実装するときに意識しやすいのだが、16bit だとそこそこ大きい値でも大丈夫なので忘れがちだ。
ええ、忘れてやらかしたことが何度かありますよ。 なので stdint.h が出てくるまでは各プロジェクトで型を定義することが多かった。

話は外れるが、x86 というか 8086 だとアセンブラで WORD が 16bit、 DWORD が 32bit だったけど、 SH4 だったかでは WORD が 32bit で HWORD が 16bit ということがあった。
8086 は 16bit なので “WORD” はバス幅通り 16bit。その倍である Double-WORD が 32bit だった。
SH-4 は 32bit だったから “WORD” が 32bit、16bit は半分ということで Half-WORD になっていた。

引数で結果を受ける場合に前にするか後ろにするか

関数の戻り値は ret = myfunc() みたいになるが、C言語では値を 1つしか戻せないので戻り値では処理が成功したかどうかだけにし、実際の結果は引数で受け取るようにすることがある。
そのとき、受け取る変数を引数の並びで前側にするか後ろ側にするかで悩んでいる。

前側にしたいと考えるのは memcpy() などで前が destination、後が source になっているからだ。
標準に合わせておけば無難だろう。

C言語には class がないので似たようなことをしたかったら引数で渡す。
C++ も隠れて第1引数に this があるようなものだから、最初に置きたい。
fprintf()FILE* も第1引数だし。
そうなると、第1引数にそういった変数を持ってきて第2引数を先頭と見なしてそこに結果用の変数を置くのが良いのか?
それだったら前半はそういう INPUT系の引数、後半を結果用の引数にすると収まりが良いような気がする。

そういう悩みだ。

しかし fwrite() は一番最後に FILE* が置かれるので、そういった変数を先頭に置くとは限らないのか。
なんで同じ FILE* 系の関数なのに前やら後ろやらバラバラにしたのかね。
可変長引数の fprintf() は後ろに並べられないから仕方なくなのか。 でもそれなら fwrite() なども先頭に置けば良いだけだ。

うむ、わからんな。

FILE* のような状態を持つ変数は第1引数に取りあえず配置して、それ以降は何事もなかったように “output系” “input系” の順番で並べるようにしよう。

アドレスとサイズ

バイナリデータを扱っていると、データそのものだけではサイズが分からないのでデータ長も一緒に管理したいと思った。

struct buffer {
  uint8_t *data;
  size_t len;
};

こういう構造体を作ると、関数の引数でアドレスとサイズを渡しているところもこれを使おうかと思う。

int decode(struct buffer *dec, const struct buffer *data);

しかし、自分以外の関数では普通にアドレスとサイズで扱っているので、 それをこの関数で使いたいとなるとわざわざ struct buffer に代入しないといけない。
そこに、むむ、と割り切れない気持ちを感じてしまった。

オーバーロードできるなら定義してしまえば良いのだが、それはできない。

int decode(struct buffer *dec, const uint8_t *data, size_t len);
int decode(struct buffer *dec, const struct buffer *data);

golang もそうだけど名前を付けるしかない。
関数を作っても、中身はこんな感じだろう。

int decode_data(struct buffer *dec, const uint8_t *data, size_t len)
{
    ...
}

int decode_buffer(struct buffer *dec, const struct buffer *data)
{
    return decode_data(dec, data->data, data->len);
}

それだったらアドレスとサイズを渡すタイプだけ作っておけばいいやん、という気持ちになるのだ。
どうしたものか。

< Top page