Javaのバイトコードで遊ぶ (Hello World)

ASMを使ってJavaバイトコードを生成して遊んでみます。 まずはその準備について。 Javaのバージョンは1.8.0_60、ASMのバージョンは5.0.3です。

生成したバイトコードを実行したり、クラスファイルに書き込みたいので そのためのユーティリティ(ExampleUtil)を準備します。 生成するクラスが持つmainメソッドを実行することにします。

private static class MyClassLoader extends ClassLoader {
  public Class<?> defineClass(String name, byte[] b) {
    return defineClass(name, b, 0, b.length);
  }
}

public static void execMain(String className, byte[] bytes)
    throws IllegalAccessException, IllegalArgumentException,
    NoSuchMethodException, SecurityException {
  MyClassLoader myClassLoader = new MyClassLoader();
  Class<?> c = myClassLoader.defineClass(className, bytes);
  try {
    c.getMethod("main", String[].class).invoke(null,
        new Object[] { new String[0] });
  } catch (InvocationTargetException e) {
    e.getCause().printStackTrace();
  }
}

public static void write(String className, byte[] bytes) throws IOException {
  File dir = new File("target" + File.separator + "output");
  if (!dir.exists())
    dir.mkdirs();
  DataOutputStream out = new DataOutputStream(
      new FileOutputStream(new File(dir, className + ".class")));
  out.write(bytes);
  out.flush();
  out.close();
}

次にバイトコードを生成する処理です。

private static byte[] generate(String className) {
  ClassWriter cw = new ClassWriter(0);
  MethodVisitor mv;

  cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null,
      "java/lang/Object", null);

  // constructor
  mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
  mv.visitCode();
  mv.visitVarInsn(ALOAD, 0);
  mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V",
      false);
  mv.visitInsn(RETURN);
  mv.visitMaxs(1, 1);
  mv.visitEnd();

  // main
  mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main",
      "([Ljava/lang/String;)V", null, null);
  mv.visitCode();

  mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
      "Ljava/io/PrintStream;");

  // println
  mv.visitLdcInsn("Hello world.");
  mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
      "(Ljava/lang/Object;)V", false);

  mv.visitInsn(RETURN);
  mv.visitMaxs(2, 1);
  mv.visitEnd();
  cw.visitEnd();

  return cw.toByteArray();
}

generateメソッドが返すbyte[]を受け取って、実行したり、ファイルに出力したりする部分。

public static void main(String... args) throws Exception {
  String className = "HelloWorld";
  byte[] bytes = generate(className);
  ExampleUtil.execMain(className, bytes);
  ExampleUtil.write(className, bytes);
}

これを実行して、「Hello world.」という文字列が表示されればOKです。 出力したクラスファイルを実行するには以下のコマンド。

$ java HelloWorld

Hello world.

出力したクラスファイルの中身を覗く場合は以下。オプションは適宜変更すればOKです。

$ javap -v -p -s HelloWorld

Classfile /home/vagrant/sandbox/workspaces/asm_practice/AsmPlaying/target/output/HelloWorld.class
  Last modified 2015/10/13; size 341 bytes
  MD5 checksum 01828f9e45f7fe516438e7d3c51b04f4
public abstract class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Utf8               HelloWorld
   #2 = Class              #1             // HelloWorld
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               java/lang/System
  #12 = Class              #11            // java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8               Hello world.
  #18 = String             #17            // Hello world.
  #19 = Utf8               java/io/PrintStream
  #20 = Class              #19            // java/io/PrintStream
  #21 = Utf8               println
  #22 = Utf8               (Ljava/lang/Object;)V
  #23 = NameAndType        #21:#22        // println:(Ljava/lang/Object;)V
  #24 = Methodref          #20.#23        // java/io/PrintStream.println:(Ljava/lang/Object;)V
  #25 = Utf8               Code
{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #18                 // String Hello world.
         5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
         8: return
}

これをベースに次回からいろいろなバイトコードを生成して遊んでみたいと思います。 最後にコードの全体を載せておきます。

ExampleUtil

package example;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

public class ExampleUtil {

  private static class MyClassLoader extends ClassLoader {
    public Class<?> defineClass(String name, byte[] b) {
      return defineClass(name, b, 0, b.length);
    }
  }

  public static void execMain(String className, byte[] bytes)
      throws IllegalAccessException, IllegalArgumentException,
      NoSuchMethodException, SecurityException {
    MyClassLoader myClassLoader = new MyClassLoader();
    Class<?> c = myClassLoader.defineClass(className, bytes);
    try {
      c.getMethod("main", String[].class).invoke(null,
          new Object[] { new String[0] });
    } catch (InvocationTargetException e) {
      e.getCause().printStackTrace();
    }
  }

  public static void write(String className, byte[] bytes) throws IOException {
    File dir = new File("target" + File.separator + "output");
    if (!dir.exists())
      dir.mkdirs();
    DataOutputStream out = new DataOutputStream(
        new FileOutputStream(new File(dir, className + ".class")));
    out.write(bytes);
    out.flush();
    out.close();
  }

}

HelloWorld

package example;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class HelloWorld implements Opcodes {
  private static byte[] generate(String className) {
    ClassWriter cw = new ClassWriter(0);
    MethodVisitor mv;

    cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null,
        "java/lang/Object", null);

    // constructor
    mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    mv.visitCode();
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V",
        false);
    mv.visitInsn(RETURN);
    mv.visitMaxs(1, 1);
    mv.visitEnd();

    // main
    mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main",
        "([Ljava/lang/String;)V", null, null);
    mv.visitCode();

    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
        "Ljava/io/PrintStream;");

    // println
    mv.visitLdcInsn("Hello world.");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
        "(Ljava/lang/Object;)V", false);

    mv.visitInsn(RETURN);
    mv.visitMaxs(2, 1);
    mv.visitEnd();
    cw.visitEnd();

    return cw.toByteArray();
  }

  public static void main(String... args) throws Exception {
    String className = "HelloWorld";
    byte[] bytes = generate(className);
    ExampleUtil.execMain(className, bytes);
    ExampleUtil.write(className, bytes);
  }
}

Fixity Resolution

https://www.haskell.org/onlinereport/haskell2010/haskellch10.html#x17-18100010.6

Haskellのリファレンスに掲載されている 結合性の解決方法を示したコードを動作確認のために動かした時のメモ。

以下は上記のページに掲載されているコード。

import Control.Monad
 
type Prec   = Int  
type Var    = String  
 
data Op = Op String Prec Fixity  
  deriving (Eq,Show)  
 
data Fixity = Leftfix | Rightfix | Nonfix  
  deriving (Eq,Show)  
 
data Exp = Var Var | OpApp Exp Op Exp | Neg Exp  
  deriving (Eq,Show)  
 
data Tok = TExp Exp | TOp Op | TNeg  
  deriving (Eq,Show)  
 
resolve :: [Tok] -> Maybe Exp  
resolve tokens = fmap fst $ parseNeg (Op "" (-1) Nonfix) tokens  
  where  
    parseNeg :: Op -> [Tok] -> Maybe (Exp,[Tok])  
    parseNeg op1 (TExp e1 : rest)  
       = parse op1 e1 rest  
    parseNeg op1 (TNeg : rest)  
       = do guard (prec1 < 6)  
            (r, rest') <- parseNeg (Op "-" 6 Leftfix) rest  
            parse op1 (Neg r) rest'  
       where  
          Op _ prec1 fix1 = op1  
 
    parse :: Op -> Exp -> [Tok] -> Maybe (Exp, [Tok])  
    parse _   e1 [] = Just (e1, [])  
    parse op1 e1 (TOp op2 : rest)  
       -- case (1): check for illegal expressions  
       | prec1 == prec2 && (fix1 /= fix2 || fix1 == Nonfix)  
       = Nothing  
 
       -- case (2): op1 and op2 should associate to the left  
       | prec1 > prec2 || (prec1 == prec2 && fix1 == Leftfix)  
       = Just (e1, TOp op2 : rest)  
 
       -- case (3): op1 and op2 should associate to the right  
       | otherwise  
       = do (r,rest') <- parseNeg op2 rest  
            parse op1 (OpApp e1 op2 r) rest'  
       where  
         Op _ prec1 fix1 = op1  
         Op _ prec2 fix2 = op2 

以下はテスト用のトークンを適当に定義したコード。

plus = Op "+" 6 Leftfix
minus = Op "-" 6 Leftfix
ast = Op "*" 7 Leftfix
slash = Op "/" 7 Leftfix

app = Op "->" 1 Rightfix
rplus = Op "+." 6 Rightfix

a = Var "a"
b = Var "b"
c = Var "c"
d = Var "d"

-- a + b +. c
-- -> illegal expressions
tokens1 = [TExp a, TOp plus, TExp b, TOp rplus, TExp c]

-- a + b - c
-- -> (a + b) - c
tokens2 = [TExp a, TOp plus, TExp b, TOp minus, TExp c]

-- a -> b -> c
-- -> a -> (b -> c)
tokens3 = [TExp a, TOp app, TExp b, TOp app, TExp c]

-- -a + b
-- -> (-a) + b
tokens4 = [TNeg, TExp a, TOp plus, TExp b]

-- a + -b
-- -> illegal expressions
tokens5 = [TExp a, TOp plus, TNeg, TExp b]

-- a + b * c - d
-- -> (a + (b * c)) - d
tokens6 = [TExp a, TOp plus, TExp b, TOp ast, TExp c, TOp minus, TExp d]

-- a + b -> c
-- -> (a + b) -> c
tokens7 = [TExp a, TOp plus, TExp b, TOp app, TExp c]

-- a -> b + c
-- -> a -> (b + c)
tokens8 = [TExp a, TOp app, TExp b, TOp plus, TExp c]

このコードをghciで読み込み、以下のように入力すれば結果が返ってくる。

resolve tokens1
resolve tokens2
...

xkbを使って修飾キー有のキーバインドを設定する

Windowsキーバインド設定ツールとして以前から KeyExtension というフリーソフトを使わせてもらっています。 変換キー + U でHome、変換キー + H でLeft、といった感じの設定ができます。 Vimでコードを書く時は困らないのですが、Eclipseなど他のエディタで作業するような時は カーソル移動が楽になるので大変愛用しています。

これと同じことを Elementary OS で実現できないかと調べたところ、 xkbを使うことで可能になるようです。早速試してみました。

設定したいキーバインドは以下です。

キー 動作
変換キー + U Home
変換キー + O End
変換キー + H Left
変換キー + J Down
変換キー + K Up
変換キー + L Right
変換キー + ; Delete
変換キー + B BackSpace
変換キー + N Return

まずは、変換キーを押した時に修飾キー(Mod5)として認識させます。

$ cat ~/.xkb/compat/mycompat
xkb_compatibility "henkan" {
  interpret Henkan_Mode { action = SetMods(modifiers=Mod5); };
};

キーバインドに使う型として、Shiftと変換キー(Mod5)を組み合わせた型として定義します。 Shift+の時はLevel2に設定した動作、変換キー+およびShift+変換キー+*の時はLevel3に設定した動作をします。

$ cat ~/.xkb/types/mytypes
xkb_types "henkan" {
  type "HENKAN" {
    modifiers = Shift+Mod5;
    map[Shift] = Level2;
    map[Mod5] = Level3;
    map[Shift+Mod5] = Level3;
    level_name[Level1] = "Base";
    level_name[Level2] = "Shift";
    level_name[Level3] = "Henkan";
  };
};

キーバインドを設定します。 左から順に、修飾キーなし(Level1)、Shiftキーあり(Level2)、変換キーあり(Level3)、の動作を設定します。 HomeやBackSpaceのところに何を書けば良いかわからないときは $ xev で目的のキーを打ち込んでみて表示されるkeysymを確認すれば良いようです。

$ cat ~/.xkb/symbols/mysymbols
xkb_symbols "henkanmodekeys" {
  key <AD07> { type="HENKAN", [ u, U, Home            ] };
  key <AD09> { type="HENKAN", [ o, O, End         ] };

  key <AC06> { type="HENKAN", [ h, H, Left            ] };
  key <AC07> { type="HENKAN", [ j, J, Down            ] };
  key <AC08> { type="HENKAN", [ k, K, Up          ] };
  key <AC09> { type="HENKAN", [ l, L, Right           ] };
  key <AC10> { type="HENKAN", [ semicolon, plus, Delete   ] };

  key <AB05> { type="HENKAN", [ b, B, BackSpace           ] };
  key <AB06> { type="HENKAN", [ n, N, Return      ] };
};

以上の設定を組み込むための定義を行います。 ベースとなる設定ファイルを生成して修正します。

$ setxkbmap -print > ~/.xkb/keymap/mykbd
$ cat ~/.xkb/keymap/mykbd
xkb_keymap {
    xkb_keycodes  { include "evdev+aliases(qwerty)" };
    xkb_types     { include "complete+mytypes(henkan)"  };
    xkb_compat    { include "complete+japan+mycompat(henkan)"   };
    xkb_symbols   { include "pc+jp+inet(evdev)+mysymbols(henkanmodekeys)"   };
    xkb_geometry  { include "pc(pc105)" };
};

テストします。

$ xkbcomp -I$HOME/.xkb ~/.xkb/keymap/mykbd $DISPLAY

ログイン時に反映させるために~/.bashrcに以下を追記します。

xkbcomp -I$HOME/.xkb ~/.xkb/keymap/mykbd $DISPLAY 2> /dev/null

以下のページが大変参考になりました。

Elementary OSにEclipseを導入する

インストール自体はファイルをダウンロードして適当な場所に配置すれば良いです。

ただEclipseツールチップが、背景色も文字色も黒くなってしまい、全く内容が読めず 使い物になりませんでした。

f:id:jou4:20150322115748p:plain

ウェブを検索したところ、次のページを見つけました。

http://elementary.io/answers/tooltip_color

Themeの設定ファイルを書き換えるのと、起動時の環境変数を設定する必要がありました。

Themeの設定ファイルを変更

$ sudo vim /usr/share/themes/elementary/gtk-2.0/gtkrc

背景色と文字色を変更します。

gtk_color_scheme  = "tooltip_bg_color:#000\ntooltip_fg_color:#FFF"

gtk_color_scheme  = "tooltip_bg_color:#F5F5C5\ntooltip_fg_color:#000"

に修正します。

起動時の環境変数を設定する

SWT_GTK3=0 という環境変数を渡す必要があります。

$ env SWT_GTK3=0 /home/vagrant/Programs/Eclipse/eclipse-luna/eclipse

これで解消しました。

f:id:jou4:20150322115803p:plain

デスクトップアプリケーションとして登録する

ついでにデスクトップアプリケーションとして登録しておきます。 ***.desktop というファイルを作成すれば良いです。

$ sudo vim /usr/share/applications/eclipse.desktop

内容は次のとおりです。

[Desktop Entry]
Name=Eclipse
GenericName=IDE
Comment=Development programs
TryExec=/home/vagrant/Programs/Eclipse/eclipse-luna/eclipse
Exec=env SWT_GTK3=0 /home/vagrant/Programs/Eclipse/eclipse-luna/eclipse
Terminal=false
Type=Application
Icon=/home/vagrant/Programs/Eclipse/eclipse-luna/icon.xpm
Categories=Utility;TextEditor;
StartupNotify=true
MimeType=text/plain;

これによりLauncherに表示されるようになります。Dockへの登録も可能になります。

f:id:jou4:20150322115817p:plain

Elementary OSをセットアップする

Elementary OSを自分用にセットアップした時のメモです。 英語版のまま使い、日本語が入力できれば良いという程度の要件です。

f:id:jou4:20150322114011p:plain

事前準備

VirtualBoxのゲストOSとして使う場合は、3Dアクセラレーションを 有効化しておきます。これをしておかないと、OS起動中ずっと Gala という Window Manager がCPUを使いまくるためです。

全パッケージ更新

全パッケージでなくとも良いですが、最低限 slingshot-launcher は 0.7.6 以上にアップグレードしておきます。ibus などのインプットメソッド起動中に 検索文字を入力できないというバグを解消するためです。

$ sudo apt-get update
$ sudo apt-get -s dist-upgrade
$ sudo apt-get -u dist-upgrade

言語設定

System SettingsLanguage Support を起動すると、ダイアログが表示され 追加パッケージのインストールを聞かれるのでインストールします。

その後、言語に日本語を追加します。ibus-anthy などのパッケージがさらに インストールされます。

Mozcを使いたいので ibus-mozcをインストールします。

$ sudo apt-get install ibus-mozc
$ ibus-setup

フォント設定

Migu 1M というフォントがお気に入りなのでインストールします。

http://mix-mplus-ipa.sourceforge.jp/migu/

システムフォントの設定

elementary-tweaks をインストールします。

$ sudo apt-add-repository ppa:versable/elementary-update
$ sudo apt-get update
$ sudo apt-get install elementary-tweaks

System SettingsTweaks というメニューが追加されるので それを使ってフォントサイズなどを調整します。

その他の追加ソフトウェア

よく使うものを追加しておきます。

  • システムモニター
$ sudo apt-get install gnome-system-monitor

Dock整理

Dockを整理します。

VagrantのBoxを作成する

VagrantのBoxを作成してみました。 Windows8.1なのでHyper-Vでやろうかと思いましたが、少し試してみたところ上手くいかなかったのでおとなしくVirtualBoxを使っています。

https://docs.vagrantup.com/v2/boxes/base.html

https://docs.vagrantup.com/v2/virtualbox/boxes.html

仮想マシンの作成

VirtualBoxでマシンを新規に作成します。

ディスク

ディスクの種類はVDIでもVMDKでも良いようです。VDIで作成しても、Boxにした際に VMDKに変換されたので最初からVMDKが良いかも。 サイズ可変で最大サイズで作る、とありますが最大サイズがよくわからないので可変サイズで128GBくらいで。

メモリ

デフォルトを大きくしすぎないように、ということなので512MBで。

周辺機器

オーディオやUSBのような不要なハードウェアは無効化しておくように、とのこと。

OSのインストール

インストールのISOイメージをドライブにセットして起動。 Elementary OS (Luna 64bit)でやりました。

ユーザー設定

vagrantというユーザーを作成します。パスワードもvagrantです。 rootのパスワードもvagrantにしておくと良いようです。

ダミーの公開鍵~/.ssh/authorized_keys に追記しておきます。パーミッション~/.ssh0700~/.ssh/authorized_keys0600です。

パスワードなしでsudoできるようにしておきます。

$ sudo update-alternatives --config editor
$ sudo visudo

以下の行を末尾に追記します。

vagrant ALL=(ALL) NOPASSWD: ALL

SSHサーバー導入

SSHサーバーをインストールします。

$ sudo apt-get install ssh sysv-rc-conf
$ sudo sysv-rc-conf ssh on
$ sudo sysv-rc-conf --list | grep ssh

VirtualBox Guest Additionsのインストール

追加プログラムをインストールします。

$ sudo apt-get install linux-headers-generic build-essential dkms
$ sudo mkdir /media/cdrom
$ sudo mount /dev/cdrom /media/cdrom
$ sudo sh /media/cdrom/VBoxLinuxAdditions.run
$ sudo reboot

後述しますが、これではインストールが正しく完了しておらず VirtualBox Guest Additionsを再セットアップする必要がありました。 念を入れてこのタイミングでやっておくと良いかなと思いました。

$ sudo /etc/init.d/vboxadd setup

Vagrant Boxの作成

ここからはゲストOSをシャットダウンして、ホストOSで作業します。

Boxの作成

{vm name}VirtualBoxでつけたマシンの名前です。 {/path/to/box} は作成するBoxの出力先です。

-- vagrant package --base {vm name} --output {/path/to/box}
$ vagrant package --base elementary-64 --output elementary-64.box

Boxの登録

{box name} はBoxの登録名です。

-- vagrant box add {box name} {/path/to/box}
$ vagrant box add elementary-64-minimal elementary-64.box
$ vagrant box list

Boxからマシンを作成

適当なディレクトリを作成しBoxからマシンを作成します。

$ mkdir elem
$ cd elem
-- vagrant init {box name}
$ vagrant init elementary-64-minimal

起動・停止

$ vagrant up
$ vagrant halt

起動時にエラーが出る場合は、VirtualBox Guest Addtionsを再セットアップ

起動時に共有フォルダのマウントでエラーが出ました。

Failed to mount folders in Linux guest. This is usually because
the "vboxsf" file system is not available. Please verify that
the guest additions are properly installed in the guest and
can work properly. The command attempted was:

mount -t vboxsf -o uid=id -u vagrant,gid=getent group vagrant | cut -d: -f3
vagrant /vagrant
mount -t vboxsf -o uid=id -u vagrant,gid=id -g vagrant vagrant /vagrant

The error output from the last command was:

stdin: is not a tty
/sbin/mount.vboxsf: mounting failed with the error: No such device

ゲストOSに戻り、VirtualBox Guest Addtionsを再セットアップ、Boxの再作成を行うことで解決しました。

$ sudo /etc/init.d/vboxadd setup

びわいち

京都に住む友人と琵琶湖サイクリングに行きました。

まずは京都の橋から。ここから1号線を通って大津へ向かいます。

近江大橋のそばで朝食。雨ばかりの8月でしたがこの日は快晴。楽しいライドとなる予感がします。 大津でも路面電車が走っているんですね。親近感を覚えました。

湖岸通りを快調にとばします。交通量が多く、多少肩身の狭い思いをしながら走りました。

ここでサイコンでエラーが発生。初期化されデータが全て消失。もう何回目だ... おかげで自分の総走行距離はさっぱり分かりません。

ピエリ守山。リニューアル中?

脇に見える湖の景色が気持ちいいです。200kmの予定なのでのんびりしてもいられないのに、ついつい写真を撮ってしまいます。

昼食は長浜の翼果楼で焼鯖そーめんと焼鯖寿司。行列になる前に入れました。

昼食後は湖の北側。少し湖から離れた場所を走ります。

こんな感じで軽快に走っていましたが、どうも友人の様子がおかしい。 足の限界が近いなどと言う。まだ半分近く残っているよ…ペースを抑えてゆっくり走ります。

白髭神社

と、とうとう比良で友人がギブアップ。 びわいち(琵琶湖一周)は諦めて輪行で京都へ戻りました。

走行距離は163km。 交通量が多くヒヤヒヤすることもありましたが、ほぼ平坦な道が続き走りやすかったです。 一周できなかったのは残念。また機会があれば。

夕飯は馬野郎で打ち上げ。 馬肉と友人の練習不足ぶりを肴に楽しく過ごしました。