SystemTap ユーザプログラムのトレースで浮動小数点値を扱う


はじめに

Linuxのカーネルコンテキストでは浮動小数点演算を扱わないためSystemTapもネイティブで浮動小数点値をサポートしていないそうです。
せっかくユーザプログラムをトレースする機能があるのに何故?と思い週末に試してみました。
結局標準機能では実現できず、floatの変換を自作したので「STAP浮動小数点はあります!」みたいにグレーな結果になってしまいました。
尚、SystemTapについては以下の記事などを参考にしました。
・SystemTapの使い方(User-Space Probing)
      https://qiita.com/hana_shin/items/6bca693206bf5f887cb3
・「SystemTap で ユーザプログラムのトレース その1、2」
      https://qiita.com/t-tkd3a/items/3be2c93721fb354394bc

準備

Linuxは初めてなのでWindows7のPCに以下のような環境を作りました。

(1)VMwareインストール
    VMware Workstation 15.0.2 Player for Windows 64-bit Operating Systems.
(2)CentOSインストール
    CentOS-7-x86_64-DVD-1810.iso
(3)CentOS環境にdebuginfoインストール
    debuginfo-install kernel-3.10.0-957.el7.x86_64 kmod-kvdo-6.1.1.125-5.el7.x86_64
(4)下記のプログラムをコンパイル
    gcc -g -Wall -o target target.c

テスト用のプログラム

/home/staptest/target.c
#include <stdio.h>
#include <stdlib.h>

char *testfunc(float f) {
    static char wk[100];
    union converter{
        float fValue;
        int iValue;
    };
    union converter c;
    c.fValue = f;
    printf("float value = %g\n",c.fValue );
    sprintf(wk,"0x%x",c.iValue );
    return wk; //probeポイント(14行目)
}
int main(void) {
    printf("int value = %s\n" , testfunc(1234.56789f));
    return EXIT_SUCCESS;
}

floatの値を検証するプログラムですがSystemTapを使いtestfunc終了直前でc.fValueの値を出力してみます。

トレース用スクリプト(その1)

/home/staptest/float_test.stp
#!/usr/bin/stap

function float_string:string (val:long) 
%{
  static char wk[100];
  union converter{
        float fValue;
        long iValue;
    };
  union converter c;
  int len;
  c.iValue = STAP_ARG_val;
  len = snprintf(wk,100,"%g",(float)c.fValue );
  strlcpy(STAP_RETVALUE, wk, len+1);
%}


probe process("/home/staptest/target").statement("[email protected]:14")
{
  printf("procname=%s\n", execname())
  printf("pp=%s\n", pp())
  printf("int_value=0x%0x\n",user_int(&$c))
  printf("float_value=%s\n",float_string(user_int(&$c)))
}

このスクリプトを作るまで試行錯誤しました。
float(変数c.fValue)の値はuser-int(&$c)で取り込みます。
次にEmbedded-cというc言語の関数をスクリプトに組み込む機能があるので、float_string関数に値を渡しsprintfでfloatを文字列に変換すれば良いという発想です。
ところがスクリプトでstdio.hのincludeを以下のように指定するとエラーになります。

%{
#include <stdio.h>
%}

致命的エラー: stdio.h: そのようなファイルやディレクトリはありません
#include <stdio.h>

よって/usr/includeのstdio.hは指定しないでスクリプトを実行しました。

# stap -g float_test.stp

次に別のコンソールでtargetを実行。

# ./target

結果は以下となります。

procname=target
pp=process("/home/staptest/target").statement("testfunc@/home/staptest/target.c:14")
int_value=0x449a522c
float_value=%g

float_value=%gとなり浮動小数点値が正しく出力されません。
念のためdoubleをfloatにキャストしたのですが%gが効かないようです。%fでもだめでした。
カーネルビルダーのカスタマイズは判らないので結局標準機能での解決はあきらめました。

トレース用スクリプト(その2)

/home/staptest/float_test2.stp
#!/usr/bin/stap

function float_string:string (val:long) 
%{
  static char str[100];
  static int DIGIT = 20;
  static char DCHAR[10]={'0','1','2','3','4','5','6','7','8','9'};
  union converter{
        float fValue;
        long iValue;
   };
  union converter c;
  int len;
  long up;
  double diff;
  int i;
  c.iValue = STAP_ARG_val;
  up = (long)c.fValue;
  len = sprintf(str,"%ld",up);
  str[len++] = '.';
  diff = c.fValue - (double)up;
  if(diff < 0)
    diff = -diff;
  for(i=0;i<DIGIT;i++){
    diff *= 10;
    str[len++] = DCHAR[(int)diff];
    diff -= (int)diff;
   }
  str[len++]=0;
  strlcpy(STAP_RETVALUE, str, len);
%}

probe process("/home/staptest/target").statement("[email protected]:14")
{
  printf("procname=%s\n", execname())
  printf("pp=%s\n", pp())
  printf("float_value=%s\n",float_string(user_int(&$c)))
}

こちらは浮動小数点値を自前で文字列に変換しています。
出力結果は以下の通り、何とか浮動小数点値の形式で出力することができました。

procname=target
pp=process("/home/staptest/target").statement("testfunc@/home/staptest/target.c:14")

float_value=1234.56787109375000000000

補足

文字列に変換する部分は結果を確認するための最低限のコードです。
本当に必要な方は適当に直して下さいね。
また、関数部分は別ファイル(.stp)にして格納ディレクトリを-Iオプションで指定すれば動くようです。

蛇足(2019/2/15追加)

一応printfの"%g","%f"相当の変換ができるようにコードを修正して共通関数float_func.stpを作りました。
Embedded-Cは思いっきり嵌りましたが結果オーライです。

  ・float_string_d ・・・ コンパクト形式("%d"相当)で変換する関数
  ・float_string_f ・・・ 小数点以下桁数固定("%f"相当)で変換する関数
  ・float_string ・・・ 小数点以下桁数、出力形式を指定して変換する関数

以下のように実行して使用します。

stap -g xxxx.stp -I /home/staptest/scripts

float_func.stp本体のコードです。
本例では/home/staptest/scriptsディレクトリに格納しています。

/home/staptest/scripts/float_func.stp
#!/usr/bin/stap

/**
 * digit+1桁目を四捨五入して指定形式でfloatをstringに変換する。
 * @val 変換元float値
 * @digit 小数点以下の桁数(0~)
 * @compact 桁数固定(%f相当):0,コンパクト(%g相当):1
 * @return 変換文字列
 */
function float_string:string (val:long,digit:long,compact:long) 
%{
    static char str[256];
    static char DCHAR[10]={'0','1','2','3','4','5','6','7','8','9'};
    union converter{
        float fValue;
        long iValue;
    };
    union converter c;
    int len;
    long upStr;
    double lowerValue;
    double roundValue;
    int i;
    int compactPosi;
    c.iValue = STAP_ARG_val;
    //四捨五入
    roundValue = 0.5;
    for(i=0;i<STAP_ARG_digit;i++){
    roundValue *= 0.1;
    }
    if(c.fValue >= 0)
        c.fValue += roundValue;
    else
        c.fValue -= roundValue;
    //整数部変換
    upStr = (long)c.fValue;
    len = sprintf(str,"%ld",upStr);
    //小数部変換
    if(STAP_ARG_digit > 0){
        str[len++] = '.';
        lowerValue = c.fValue - (double)upStr;
        if(lowerValue < 0)
        lowerValue = -lowerValue;
        for(i=0;i<STAP_ARG_digit;i++){
        lowerValue *= 10;
        str[len++] = DCHAR[(int)lowerValue];
        lowerValue -= (int)lowerValue;
        }
    }
    str[len++]='\0';
    //コンパクト化指定のとき左側のゼロをサプレス
    if(STAP_ARG_compact){
        compactPosi = 0;
        for(i=len-2;i>=len-STAP_ARG_digit;i--){
            if(str[i] == '0')
                compactPosi = i;
            else
                break;
        }
            if(compactPosi > 0){
                str[compactPosi]='\0';
                len = compactPosi + 1;
        }
    }
    strlcpy(STAP_RETVALUE, str, len);
%}

/**
 * 3桁目を四捨五入して小数点以下の桁数2桁コンパクト形式でfloatをstringに変換する。
 * @val 変換元float値
 * @return 変換文字列
 */
function float_string_d(val:long)
{
    return float_string(val,2,1)
}

/**
 * 7桁目を四捨五入して小数点以下の桁数6桁固定でfloatをstringに変換する。
 * @val 変換元float値
 * @return 変換文字列
 */
function float_string_f(val:long)
{
    return float_string(val,6,0)
}

float_string_dとfloat_string_fは%{ %}で囲んでいません。
また、functionの戻り値、return文のセミコロンを省略してもエラーになりません。