Cortex-MターゲットをOpenOCD + GDBでデバッグする(gdb-dashboard or VSCode)

OpenOCDとGDBを使ったarmのCortex-Mターゲットのデバッグ方法についてまとめておく。
個人的に、CUIベースでのデバッグにはgdb-dashboardGUIベースでのデバッグにはVSCodeを使うのが好みなため、この2つのUIを介してデバッグする方法についてそれぞれ書くことにする。

なお、動作確認環境はArchLinux。

※OpenOCDでのFWの書き込み方法については、"OpenOCD + ST-LinkでFirmware書き込み"も参照。

共通で必要なもの

gdb-dashboardを使う場合も、VSCodeを使う場合も以下がそれぞれ必要になる。 ([]内は動作確認時のバージョン情報)

  • OpenOCD [0.10.0]
  • arm-none-eabi-gdb [8.1]
  • ターゲットに書き込むelfファイル
  • Cortex-Mターゲット(この記事ではSTMicroelectronicsのNUCLEO-F401REを使用)
  • ST-Linkなどのデバッガ(この記事ではNUCLEO-F401REにオンボードで載っているのでそれを使う)

gdb-dashboard

cyrus-and/gdb-dashboard: Modular visual interface for GDB in Python

f:id:ryochack:20180212230847p:plain

CUIGDBをグラフィカルに表示してくれるPyhtonスクリプト。 CUIからちょっとデバッグしたい時に便利。

インストール

以下の方法でインストールできる。(gdb-dashboardのREADMEではwgetを使っているが、更新に追従できるようにここではgit cloneを使用する)

$ git clone https://github.com/cyrus-and/gdb-dashboard.git
$ cp gdb-dashboard/.gdbinit ~/.gdbinit

デバッグ手順

まず、OpenOCDを起動する。

$ openocd -f board/st_nucleo_f4.cfg

-f board/st_nucleo_f4.cfgの部分はターゲットに応じて適宜変更する。(ArchLinuxの場合は、/usr/share/openocd/scriptsにOpenOCDの設定ファイルが配置されている)
別の端末からGDBを起動し、以下のコマンドを順次打ち込む。

$ arm-none-eabi-gdb ${target_elf_path}

>>> target remote localhost:3333
>>> interrupt
>>> monitor reset halt
>>> load

これで準備が整った。 ここからブレイクポイントを設定するなり、ステップ実行するなりしてデバッグを行う。

以下は、main.rsというファイルの7行目にブレイクポイントを設定してプログラムを実行する例となる。

>>> tbreak main.rs:7
>>> continue

VSCode + Cortex-Debug Extension

Visual Studio Code - Code Editing. Redefined

  • VSCode [1.19.0]
  • Cortex-Debug Extension [0.1.14]

f:id:ryochack:20180212230927p:plain

VSCodeからOpenOCD + GDBでCortex-Mターゲットをデバッグする場合には、Cortex-Debug extensionをインストールするのがおすすめ。 (後述するが、自前でGDBの設定をするとOpenOCDの手動で起動する必要があるが、このextensionを使うとOpenOCDを自動で起動してくれるようになる)

Cortex-Debug - Personal

gdb-dashboardをインストールして~/.gdbinitが有効になっているとVSCodeデバッグがうまく動かなくなるので、~/.gdbinit.bkなどにリネームして無効にする。

インストール

  1. VSCodeをインストールする
  2. Cortex-Debug extensionをVSCodeにインストールする

VSCodeデバッグ構成ファイルを用意する

VSCodeデバッグするためには launch.json というデバッグ構成を記載するための設定ファイルを書く必要がある。

Cortex-Debug extensionを使う際には、以下のように launch.json を書く(launch.json の修正までの手順は"デバッグ | 非公式 - Visual Studio Code Docs"の記事を参考に)

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cortex-debug",
            "servertype": "openocd",
            "request": "launch",
            "name": "OpenOCD-Debug",
            "executable": "${ターゲットELFまでのPATH}",
            "configFiles": [
                "board/st_nucleo_f4.cfg"
            ],
            "cwd": "${workspaceRoot}",
            "gdbpath": "arm-none-eabi-gdb",
        }
    ]
}

※なお、Cortex-Debug extensionはOpenOCDだけではなく、J-Linkにも対応している模様。

デバッグ手順

VSCode上でF5キーを押してデバッグを開始する。

VSCode (Cortex-Debug Extension無し)

f:id:ryochack:20180212230941p:plain

蛇足になるが、VSCodeでCortex-Debug extensionを使わない場合のやり方も残しておく。

※こちらも同様に、gdb-dashboardをインストールして~/.gdbinitが有効になっているとVSCodeデバッグがうまく動かなくなるので、~/.gdbinit.bkなどにリネームして無効にする。

VSCodeデバッグ構成ファイルを用意する

以下の launch.json を用意する。

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "OpenOCD Debug",
            "type": "gdb",
            "request": "attach",
            "executable": "${ターゲットELFまでのPATH}",
            "remote": true,
            "target": ":3333",
            "cwd": "${workspaceRoot}",
            "gdbpath": "arm-none-eabi-gdb",
            "autorun": [
                "interrupt",
                "monitor reset halt",
                "load",
            ],
        }
    ]
}

デバッグ手順

まず、OpenOCDを起動する(-f board/st_nucleo_f4.cfgの部分はターゲットに応じて適宜変更する)

$ openocd -f board/st_nucleo_f4.cfg

OpenOCDが起動できたら、VSCode上でF5キーを押してデバッグを開始する。

参考

systemd環境でのコアダンプ解析

systemd環境下においては、従来(systemd無しの環境)のコアダンプ解析とは勝手が違ってくる。

コアダンプを有効にする

ulimitコマンドでcore file sizeが0になっていないことを確認する。

$ ulimit -a
-t: cpu time (seconds)              unlimited
-f: file size (blocks)              unlimited
-d: data seg size (kbytes)          unlimited
-s: stack size (kbytes)             8192
-c: core file size (blocks)         unlimited
-m: resident set size (kbytes)      unlimited
-u: processes                       63401
-n: file descriptors                1024
-l: locked-in-memory size (kbytes)  unlimited
-v: address space (kbytes)          unlimited
-x: file locks                      unlimited
-i: pending signals                 63401
-q: bytes in POSIX msg queues       819200
-e: max nice                        0
-r: max rt priority                 99
-N 15:                              unlimited

もしも0になっていたら、次のようにしてコアダンプを有効にする。

$ ulimit -c unlimited

ここまでは従来の方法と同じ。

コアダンプの出力場所

コアダンプの出力場所は /proc/sys/kernel/core_pattern を参照するとわかる。 systemdでは、

$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %e

となっており、 /var/lib/systemd/coredump/ 以下にLZ4で圧縮されて保存される。 保存されるファイル名は core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4 といったものになる。

コアダンプの確認方法

従来の方法だと、次のコマンドでコアダンプの解析が行える。

$ gdb ./user-app core

systemd環境下で前述の方法と同様に行うと、コアダンプの認識に失敗する。

$ gdb ./user-app /var/lib/systemd/coredump/core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4
GNU gdb (GDB) 8.0.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./user-app...done.
"/var/lib/systemd/coredump/core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4" is not a core dump: File format not recognized

これは /var/lib/systemd/coredump/ 以下のコアダンプファイルがLZ4で圧縮されて保存されているためで、LZ4を展開してやればgdbから認識できるようになる。

$ lz4 -d /var/lib/systemd/coredump/core.user-app.1000.adc138b171b140d2bb0a20e628857f04.22015.1515249501000000.lz4 core
$ gdb ./user-app core

しかし、毎回コアダンプを展開するのは手間だ。 そこでsystemdでは coredumpctl というコマンドが用意されている。

coredumpctl の使い方

保存されているコアダンプ一覧を確認する。

$ coredumpctl list
TIME                            PID   UID   GID SIG COREFILE  EXE
Fri 2017-11-24 11:23:30 JST    1242     0     0  11 missing   /usr/lib/udisks2/udisksd
Sun 2017-11-26 00:12:29 JST    1959  1000  1000   5 missing   /usr/bin/ibus-daemon
Sun 2017-11-26 00:16:42 JST   10271  1000  1000   5 missing   /usr/bin/ibus-daemon
Fri 2017-12-15 18:42:18 JST    5242  1000  1000  11 missing   /usr/share/franz/franz
Tue 2017-12-19 22:49:53 JST    2933  1000  1000  11 missing   /opt/dropbox/dropbox
Thu 2017-12-21 02:14:13 JST    1723  1000  1000  11 missing   /usr/share/franz/franz
Fri 2017-12-22 02:04:45 JST    9251  1000  1000  11 missing   /usr/lib/electron/electron
Sun 2018-01-07 01:25:33 JST    6231  1000  1000   8 present   /home/ryo/code/workspace/csfml/user-app

gdbを使ってコアダンプを解析する。例えば、前述のリストから /home/ryo/code/workspace/csfml/user-app の解析をする場合には、PID 6231を指定する。

$ coredumpctl gdb 6231

参考

OpenOCD + ST-LinkでFirmware書き込み

OpenOCDを使ってSTM32F401 Nucleo-64にST-Link経由でFirmwareを書き込む。

OpenOCDとST-Linkドライバのインストール

Arch Linuxなら以下のコマンドでインストールする。

$ pacman -S openocd stlink

Firmwareを書き込む

OpenOCDで指定できるinterface, target, boardの設定ファイルは/usr/share/openocd/scriptsに配置されている。

OpenOCDで接続する

STM32F401 Nucleo-64 boardのUSB接続の場合

f:id:ryochack:20171103201550j:plain:w480

$ openocd -f board/st_nucleo_f4.cfg

もしくは、

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f4x.cfg

STM32F401 Chip + 中華ST-Linkの場合

f:id:ryochack:20171103201242j:plain:w480

Nucleoボードを使っているのなら中華ST-Linkを使う必要はないが、試した結果を書き残しておく。

$ openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg

なお、Nucleoボード上のSWDコネクタ(CN4)は以下のピンアサインとなっている。

Pin CN4
1 VDD_TARGET
2 SWCLK
3 GND
4 SWDIO
5 NRST
6 SWO

※中華ST-Linkでは、一度ST-LinkのFirmwareをアップデートした後に書き込みに成功するようになった。 (書き込みに成功している中華ST-LinkのFW versionはV2J28S7)

telnetで書き込む

上記手順でOpenOCDで接続後、別のterminalからtelnetを起動する。

$ telnet localhost 4444

telnet上で以下のコマンドを実行することでFirmwareを書き込む。

> reset halt
> flash write_image erase ${elf_file_absolute_path}

telnet無しでFirmwareを書き込む

上記の方法だと、OpenOCDとは別にtelnetを起動する必要があり、若干煩わしい。 そこで、Firmwareを書き込む手順を記載したOpenOCDのconfigファイルを用意し、OpenOCDだけで書き込みを行えるようにする。

任意の名前で以下の内容のconfigファイルを作成する。 (今回はopenocd.cfgという名前にした)

telnet_port 4444
gdb_port 3333

source [find board/st_nucleo_f4.cfg]

init

proc flash_elf {elf_file} {
    reset
    halt
    flash write_image erase $elf_file
    verify_image $elf_file
    echo "flash write_image ($elf_file) complete"
    reset
    exit
}

configファイルを作成したら、以下のコマンドで書き込みを実行する。

$ openocd -f openocd.cfg -c "flash_elf ${elf_file_relative_path}

※OpenOCDのprocの名前はOpenOCDの既存コマンド名にあるものを使うとエラーになるようだ。
(最初、proc flash {elf_file}としていたところ、以下のエラーが出て小一時間ほどハマってしまった)

Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 2000 kHz
adapter_nsrst_delay: 100
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v28 API v2 SWIM v18 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 3.256353
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
flash
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
adapter speed: 1800 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x61000000 pc: 0x1fff1048 msp: 0x20002e40
openocd.cfg:13: Error: wrong # args: should be "flash elf_file"
in procedure 'flash'
at file "openocd.cfg", line 13

ST-LinkのFirmwareアップデート

こちらで公開されているツールを使ってアップデートする。

参考

Rustでクロスコンパイル

x86_64-unknown-linux-gnuのホスト環境上で、CHIP向けのバイナリをRustでクロスコンパイルすることを目的とする。

概要

  1. rustup target add ${target_triple} でターゲットのツールチェインを追加
  2. ターゲットのリンカをインストール
  3. Cargoのconfigファイルにターゲット用のリンカ設定を記述
  4. cargo build --target ${target_triple} でクロスコンパイル実行

※Rustのツールチェイン管理を行ってくれるツールのrustupこちらを参考にインストール済みとする。

環境

ホストのシステム情報。

$ uname -a
Linux hostname 4.13.7-1-ARCH #1 SMP PREEMPT Sat Oct 14 20:13:26 CEST 2017 x86_64 GNU/Linux

$ rustup show
Default host: x86_64-unknown-linux-gnu

1.21.0-x86_64-unknown-linux-gnu (default)
rustc 1.21.0 (3b72af97e 2017-10-09)

ターゲットであるCHIPのシステム情報。

chip@chip:~$ uname -a
Linux chip 4.4.13-ntc-mlc #1 SMP Thu Nov 3 01:28:54 UTC 2016 armv7l GNU/Linux

ターゲットのツールチェインを追加

rustup target listで、Rustがサポートするターゲットのリストを確認する。

$ rustup target list | pr -w 90 --columns=2

aarch64-apple-ios                            mips-unknown-linux-gnu
aarch64-linux-android                        mips-unknown-linux-musl
aarch64-unknown-fuchsia                      mips64-unknown-linux-gnuabi64
aarch64-unknown-linux-gnu                    mips64el-unknown-linux-gnuabi64
arm-linux-androideabi                        mipsel-unknown-linux-gnu
arm-unknown-linux-gnueabi                    mipsel-unknown-linux-musl
arm-unknown-linux-gnueabihf                  powerpc-unknown-linux-gnu
arm-unknown-linux-musleabi                   powerpc64-unknown-linux-gnu
arm-unknown-linux-musleabihf                 powerpc64le-unknown-linux-gnu
armv7-apple-ios                              s390x-unknown-linux-gnu
armv7-linux-androideabi                      sparc64-unknown-linux-gnu
armv7-unknown-linux-gnueabihf                wasm32-unknown-emscripten
armv7-unknown-linux-musleabihf               x86_64-apple-darwin
armv7s-apple-ios                             x86_64-apple-ios
asmjs-unknown-emscripten                     x86_64-linux-android
i386-apple-ios                               x86_64-pc-windows-gnu
i586-pc-windows-msvc                         x86_64-pc-windows-msvc
i586-unknown-linux-gnu                       x86_64-rumprun-netbsd
i686-apple-darwin                            x86_64-unknown-freebsd
i686-linux-android                           x86_64-unknown-fuchsia
i686-pc-windows-gnu                          x86_64-unknown-linux-gnu (default)
i686-pc-windows-msvc                         x86_64-unknown-linux-musl
i686-unknown-freebsd                         x86_64-unknown-netbsd
i686-unknown-linux-gnu                       x86_64-unknown-redox
i686-unknown-linux-musl

armv7-unknown-linux-gnueabihfが見つかり、RustはCHIPのターゲットをサポートしていることを確認できたため、以下のコマンドでCHIP用のツールチェインを追加する。

$ rustup target add armv7-unknown-linux-gnueabihf

rustup target add後にrustup showrustup target listを実行してRustのツールチェインの状態を確認すると、armv7-unknown-linux-gnueabihf(installed)になっているのがわかる。

$ rustup show
Default host: x86_64-unknown-linux-gnu

installed targets for active toolchain
--------------------------------------

armv7-unknown-linux-gnueabihf
x86_64-unknown-linux-gnu

active toolchain
----------------

1.21.0-x86_64-unknown-linux-gnu (default)
rustc 1.21.0 (3b72af97e 2017-10-09)

$ rustup target list | pr -w 90 --columns=2

aarch64-apple-ios                            i686-unknown-linux-musl
aarch64-linux-android                        mips-unknown-linux-gnu
aarch64-unknown-fuchsia                      mips-unknown-linux-musl
aarch64-unknown-linux-gnu                    mips64-unknown-linux-gnuabi64
arm-linux-androideabi                        mips64el-unknown-linux-gnuabi64
arm-unknown-linux-gnueabi                    mipsel-unknown-linux-gnu
arm-unknown-linux-gnueabihf                  mipsel-unknown-linux-musl
arm-unknown-linux-musleabi                   powerpc-unknown-linux-gnu
arm-unknown-linux-musleabihf                 powerpc64-unknown-linux-gnu
armv7-apple-ios                              powerpc64le-unknown-linux-gnu
armv7-linux-androideabi                      s390x-unknown-linux-gnu
armv7-unknown-linux-gnueabihf (installed)    sparc64-unknown-linux-gnu
armv7-unknown-linux-musleabihf               wasm32-unknown-emscripten
armv7s-apple-ios                             x86_64-apple-darwin
asmjs-unknown-emscripten                     x86_64-apple-ios
i386-apple-ios                               x86_64-linux-android
i586-pc-windows-msvc                         x86_64-pc-windows-gnu
i586-unknown-linux-gnu                       x86_64-pc-windows-msvc
i686-apple-darwin                            x86_64-rumprun-netbsd
i686-linux-android                           x86_64-unknown-freebsd
i686-pc-windows-gnu                          x86_64-unknown-fuchsia
i686-pc-windows-msvc                         x86_64-unknown-linux-gnu (default)
i686-unknown-freebsd                         x86_64-unknown-linux-musl
i686-unknown-linux-gnu                       x86_64-unknown-netbsd

ターゲットのリンカをインストール

Rustのツールチェインではターゲットのリンカはサポートしていないため、別途用意する。

$ apt-get install gcc-arm-linux-gnueabihf

Cargoのconfigにターゲットのリンカ設定を記述

ロスコンパイルする対象とするプロジェクトを生成する。

$ cargo new --bin hello

Cargoが生成したスケルトンコードのままで実行すると、Hello, world!が標準出力に出力される。

$ cargo run
   Compiling hello v0.1.0 (file:///host_shared/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.44 secs
     Running `target/debug/hello`
Hello, world!

これをそのままターゲット(CHIP)上で動作するようにするために、プロジェクトのconfigファイルを作成し、ターゲットのリンカの設定を記述する。

$ mkdir .cargo
$ cat > .cargo/config << EOF
> [target.armv7-unknown-linux-gnueabihf]
> linker = "arm-linux-gnueabihf-gcc"
> EOF

ロスコンパイル実行

$ cargo build --target armv7-unknown-linux-gnueabihf

ビルドされたバイナリの情報を確認すると、ターゲット(CHIP)向けのバイナリになっていることがわかる。

$ file target/armv7-unknown-linux-gnueabihf/debug/hello
target/armv7-unknown-linux-gnueabihf/debug/hello: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=70dfb01aa67d797fc64005ac8cff1dba94a48a34, not stripped

$ readelf -A target/armv7-unknown-linux-gnueabihf/debug/hello
Attribute Section: aeabi
File Attributes
  Tag_CPU_name: "7-A"
  Tag_CPU_arch: v7
  Tag_CPU_arch_profile: Application
  Tag_ARM_ISA_use: Yes
  Tag_THUMB_ISA_use: Thumb-2
  Tag_FP_arch: VFPv3-D16
  Tag_ABI_PCS_GOT_use: GOT-indirect
  Tag_ABI_PCS_wchar_t: 4
  Tag_ABI_FP_rounding: Needed
  Tag_ABI_FP_denormal: Needed
  Tag_ABI_FP_exceptions: Needed
  Tag_ABI_FP_number_model: IEEE 754
  Tag_ABI_align_needed: 8-byte
  Tag_ABI_enum_size: int
  Tag_ABI_VFP_args: VFP registers
  Tag_CPU_unaligned_access: v6
  Tag_ABI_FP_16bit_format: IEEE 754

ロスコンパイルしたバイナリをターゲット上で実行

chip@chip:~$ ./hello
Hello, world!

ターゲット(CHIP)上で実行できることが確認できた。

参考

Rustでのクロスコンパイルに必要な情報が、以下のページに非常にわかりやすく説明されている。
japaric/rust-cross: Everything you need to know about cross compiling Rust programs!

環境構築の検証用にDockerを使う

Rustのクロスコンパイル環境構築の検証にDockerを使い始めた。

開発環境を構築する際、最初は試行錯誤しながら行なうため、さぁ環境が構築ができた、となった時に行なってきた手順のどれが最低限必要なものだったのかがわからなくなることが多い。 必要だと思ってインストールしたものが実は必要なかったり、必要ないと思ったものに実は依存していたりするため、手順に再現性があるのかどうかを検証したくなる。

このようなケースにおいて、いつでもクリーンな環境から手順の確認ができるDockerが非常に便利だったので、以下にRustの開発環境をDockerで立ち上げるまでにやったことをメモとして残しておく。

$ docker --version
Docker version 17.10.0-ce, build f4ffd2511c

Docker HubでRustがインストールされたDockerイメージが公開されているのでpullする。

$ docker pull rust

Rustのイメージが取得できたことを確認する。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
rust                latest              9578919665db        7 days ago          1.33GB

RustのDockerイメージからコンテナを起動する。

$ docker run -i -t -v ${host_dir_path}:${container_dir_path} --rm rust

docker runのオプションの意味は以下を参照。

  • -i : 標準入力を有効にする
  • -t : 疑似ターミナルを割り当てる
  • -v ${host_dir_path}:${container_dir_path} : ホストとコンテナの共有ディレクトリを指定する
  • --rm : コンテナからExitした時に自動的にコンテナを削除する

共有ディレクトリを指定しているのは、ホストで書いたコードをコンテナ内でビルドし、コンテナでビルドしたファイルをホストから参照するのを容易にするため。