2019年6月20日木曜日

ArduinoIDEでCとアセンブリ同時出力したい

この記事は以下の環境の話です。
  Arduino UNO/Nano/Mega
  AVR 328P
  ArduinoIDE 1.8.9 (非WindowsStore版)
  Windows 10 Pro/Home Version 1903

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Arduino Uno Rev3
価格:3234円(税込、送料別) (2019/6/26時点)


■アセンブリ出力が欲しい

~発端~
ArduinoIDEでプログラムを作成していると、switch-caseで10個ぐらい分岐したときに8つ目以降で分岐できないものが出てきたりして、原因を掴むためにアセンブリ出力がどうしても欲しくなった。(アセンブリ時点で分岐項目がなくなっていたのでバグのようだった)
その後、カウンターで時計のプログラムを書いているとどうしても時間がずれてくる。出力された機械語がどうなっているかでクロック消費量が違ってくるのでそこも考慮してプログラムを書かないと正確な時計にならない。

というわけで、
 Cとアセンブラ同時出力があればいろいろわかって便利がいいぞ!
となりまして、いろいろネットをさまよっていると、これまたいろいろ方法があるわけで。Evernoteに保存していたネットの情報をもとにもう一度その手順を追おうとしてももう手元の情報だけで手順がわからない。
ほんと開発メモは大事ですね。

次のコードのアセンブリを出力したいと思います。

[echo.ino]
int lamp = 0;

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
  if (Serial.available()) {
    int inByte = Serial.read();
    Serial.write(inByte);
    digitalWrite(LED_BUILTIN, (lamp ++) & 1);   // turn the LED on or off
  }
}

【手順0】中間ファイルを出力させる preferences.txt

いずれの方法でも、ビルド時に出力される中間ファイルを入手できなければなにもできません。
デフォルトの保存場所は
C:\Documents and Settings\<ユーザー名>\Local Settings\Temp\build*.tmp
だったりしますが、出力先を指定しておくと便利です。
中間ファイルの出力先の変更は、環境設定を変更しなければなりません。この環境設定はArduinoIDEの中にはなく、設定ファイル[preferences.txt]に記載されています。このファイルの保存場所は次の所になります。

[preferences.txt]の場所
%UserProfile%\AppData\Local\Arduino15\preferences.txt

ユーザーがOwnerならば
C:\Users\Owner\AppData\Local\Arduino15\preferences.txt
あたりになります。

ArduinoIDEを終了してから編集します。
次の行を追加します。
[preferences.txt]
build.path=%UserProfile%\Arduino\build

もともと build.path=... は存在しないので、最終行でもいいので追加します。

ArduinoIDEでなにかスケッチをビルドすると
%UserProfile%\Arduino\build
内にファイルが出力されます。

スケッチのあるフォルダに出力する方法もあったようですが。
[preferences.txt]
build.path=build

参考にしたホームページには、上記のような記述でスケッチのフォルダ内にbuildフォルダを作成し保存する。とあったのですが、私の環境ではコンパイラの存在する場所のbuildフォルダに出力しようとし、動作しませんでした。どうもビルド時の動作を見ると、このbuildフォルダにスケッチフォルダの中身全てをコピーしてから作業するようで、スケッチフォルダにいろいろファイルを作成しているとビルド動作が遅くなります。
ただ、今後のアップデートでは、この記述でも動作するかもしれません。

【方法1】avr-objdump -S を使ってアセンブリを出力する


中間ファイルの出力先にelfファイルが出力されています。avr-objdumpを使うことで、このelfファイルからCのソースコード付きアセンブリを出力できます。
スイッチに-Sをつけてアセンブリを出力します。

avr-objdump -S [スケッチ名.ino.elf]  > [出力ファイル名]

実際のコマンドは次のようになります。
インストール環境によりパスは変わると思います。

C:/Program Files (x86)/Arduino/hardware/tools/avr/bin/avr-objdump -S echo.ino.elf > echo.ino.S

実際に出力されたアセンブリはこちら。
[echo.ino.S]
echo.ino.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0: 0c 94 5d 00 jmp 0xba ; 0xba <__ctors_end>
   4: 0c 94 85 00 jmp 0x10a ; 0x10a <__bad_interrupt>
   8: 0c 94 85 00 jmp 0x10a ; 0x10a <__bad_interrupt>
 :
   :
void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
  if (Serial.available()) {
 5a8: 84 e1        ldi r24, 0x14 ; 20
 5aa: 91 e0        ldi r25, 0x01 ; 1
 5ac: 0e 94 e6 00 call 0x1cc ; 0x1cc <_ZN14HardwareSerial9availableEv>
 5b0: 89 2b        or r24, r25
 5b2: 09 f4        brne .+2      ; 0x5b6 <main+0x140>
 5b4: 58 c0        rjmp .+176    ; 0x666 <main+0x1f0>
    int inByte = Serial.read();
 5b6: 84 e1        ldi r24, 0x14 ; 20
 5b8: 91 e0        ldi r25, 0x01 ; 1
 5ba: 0e 94 c4 00 call 0x188 ; 0x188 <_ZN14HardwareSerial4readEv>
    virtual void flush(void);
    virtual size_t write(uint8_t);
    inline size_t write(unsigned long n) { return write((uint8_t)n); }
    inline size_t write(long n) { return write((uint8_t)n); }
    inline size_t write(unsigned int n) { return write((uint8_t)n); }
    inline size_t write(int n) { return write((uint8_t)n); }
 5be: 68 2f        mov r22, r24
 5c0: 84 e1        ldi r24, 0x14 ; 20
 5c2: 91 e0        ldi r25, 0x01 ; 1
 5c4: 0e 94 1e 01 call 0x23c ; 0x23c <_ZN14HardwareSerial5writeEh>
    Serial.write(inByte);
    digitalWrite(LED_BUILTIN, (lamp ++) & 1);   // turn the LED on or off
 5c8: 20 91 12 01 lds r18, 0x0112 ; 0x800112 <__data_end>
 5cc: 30 91 13 01 lds r19, 0x0113 ; 0x800113 <__data_end+0x1>
 5d0: c9 01        movw r24, r18
 5d2: 01 96        adiw r24, 0x01 ; 1
 5d4: 90 93 13 01 sts 0x0113, r25 ; 0x800113 <__data_end+0x1>
 5d8: 80 93 12 01 sts 0x0112, r24 ; 0x800112 <__data_end>
 5dc: f7 01        movw r30, r14
 5de: 84 91        lpm r24, Z
uint8_t bit = digitalPinToBitMask(pin);
 5e0: fe 01        movw r30, r28
 5e2: 94 91        lpm r25, Z
uint8_t port = digitalPinToPort(pin);
 5e4: f8 01        movw r30, r16
 5e6: 44 91        lpm r20, Z
volatile uint8_t *out;

if (port == NOT_A_PIN) return;
 5e8: 44 23        and r20, r20
 5ea: 09 f4        brne .+2      ; 0x5ee <main+0x178>
 5ec: 3c c0        rjmp .+120    ; 0x666 <main+0x1f0>

// If the pin that support PWM output, we need to turn it off
// before doing a digital write.
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
 5ee: 88 23        and r24, r24
 5f0: 39 f1        breq .+78      ; 0x640 <main+0x1ca>
//

これこれ!非常に読みやすいですね。
参考にしたのは以下のページです。ありがとうございました。



以下、Cのソースコードは一緒に出てきませんがアセンブリを出力する他の方法です。

【方法2】Cコンパイラからアセンブリを直接出力させる

もっと簡単な方法で出力できたような気がするのですが、Cコンパイラの出力からアセンブリを直接出力させる方法です。面倒くさいですがいろいろ応用ができそうなのでここに書いておきます。

以下スケッチファイルを[echo.ino]として説明します。
次のようにオプションを指定してビルドするとアセンブリを出力できます。

"C:\\Program Files (x86)\\Arduino\\hardware\\tools\\avr/bin/avr-g++" -S -g  -Os -std=gnu++11  -Wno-error=narrowing  -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10809 -DARDUINO_AVR_NANO -DARDUINO_ARCH_AVR "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\cores\\arduino" "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\variants\\eightanaloginputs" "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.cpp" -o "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.S"

このコマンドでbuildフォルダにアセンブリファイルを出力します。

このコマンドはビルドオプション"-S -g Os -o -std=gnu++11  -Wno-error=narrowing"が必須になります。また、MCUの種類、速度、ArduinoNo等を指定していますが、このあたりは環境が違うと異なってくるパラメーターです。このパラメーターを取得するにはArduinoIDEよりコンパイルオプションを表示させて流用すると楽です。

ArduinoIDE>ファイル>環境設定
  より詳細な情報を表示する[v]コンパイル

上記画面のコンパイルにチェックをいれてビルドします。

ビルドすると出力窓にコンパイルオプション付きのコマンドが出力されていきますので、それをすべてコピーし、メモ帳などに貼り付けて[スケッチ名.ino.cpp]を探します。この場合ですと、[echo.ino.cpp]を探します。

"C:\\Program Files (x86)\\Arduino\\hardware\\tools\\avr/bin/avr-g++" -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -flto -w -x c++ -E -CC -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10809 -DARDUINO_AVR_NANO -DARDUINO_ARCH_AVR  "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\cores\\arduino" "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\variants\\eightanaloginputs" "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.cpp" -o null

オプション追加 -S
出力先追加 -o "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.S"
上記オレンジ部分のオプション削除

"C:\\Program Files (x86)\\Arduino\\hardware\\tools\\avr/bin/avr-g++" -S -g  -Os -std=gnu++11  -Wno-error=narrowing  -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10809 -DARDUINO_AVR_NANO -DARDUINO_ARCH_AVR "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\cores\\arduino" "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\variants\\eightanaloginputs" "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.cpp" -o "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.S"

でビルドすると先の項目【中間ファイルを出力する】で設定した
[preferences.txt]
build.path=%UserProfile%\Arduino\build

パスにecho.ino.Sが出力されていると思います。
出力結果
[echo.ino.S]
 .file "echo.ino.cpp" __SP_H__ = 0x3e __SP_L__ = 0x3d __SREG__ = 0x3f __tmp_reg__ = 0 __zero_reg__ = 1 ; GNU C++11 (GCC) version 5.4.0 (avr) ; compiled by GNU C version 4.8.2, GMP version 5.0.2, MPFR version 3.0.0, MPC version 0.9       :
      :
  .text .Ltext0: .cfi_sections .debug_frame .global setup .type setup, @function setup: .LFB112: .file 1 "C:\\Users\\Owner\\Arduino\\echo\\echo.ino" .loc 1 3 0 .cfi_startproc /* prologue: function */ /* frame size = 0 */ /* stack size = 0 */ .L__stack_usage = 0 .LVL0: .LBB4: .LBB5: .file 2 "c:\\program files (x86)\\arduino\\hardware\\arduino\\avr\\cores\\arduino\\HardwareSerial.h" .loc 2 121 0 ldi r18,lo8(6) ; , ldi r20,lo8(-128) ; , ldi r21,lo8(37) ; , ldi r22,0 ; ldi r23,0 ; ldi r24,lo8(Serial) ; , ldi r25,hi8(Serial) ; , call _ZN14HardwareSerial5beginEmh ; .LVL1: .LBE5: .LBE4: .loc 1 5 0 ldi r22,lo8(1) ; , ldi r24,lo8(13) ; , jmp pinMode ; .LVL2: .cfi_endproc .LFE112: .size setup, .-setup .global loop .type loop, @function loop: .LFB113: .loc 1 7 0 .cfi_startproc /* prologue: function */ /* frame size = 0 */ /* stack size = 0 */ .L__stack_usage = 0 .LBB10: .loc 1 8 0 ldi r24,lo8(Serial) ; , ldi r25,hi8(Serial) ; , call _ZN14HardwareSerial9availableEv ; .LVL3: or r24,r25 ; breq .L2 ; , .LBB11: .loc 1 9 0 ldi r24,lo8(Serial) ; , ldi r25,hi8(Serial) ; , call _ZN14HardwareSerial4readEv ; .LVL4: .LBB12: .LBB13: .loc 2 133 0 mov r22,r24 ; , inByte ldi r24,lo8(Serial) ; , ldi r25,hi8(Serial) ; , .LVL5: call _ZN14HardwareSerial5writeEh ; .LVL6: .LBE13: .LBE12: .loc 1 11 0 lds r22,lamp ; D.4204, lamp lds r23,lamp+1 ; D.4204, lamp movw r24,r22 ; D.4204, D.4204 adiw r24,1 ; D.4204, sts lamp+1,r25 ; lamp, D.4204 sts lamp,r24 ; lamp, D.4204 andi r22,lo8(1) ; D.4205, ldi r24,lo8(13) ; , jmp digitalWrite ; .LVL7: .L2: ret .LBE11: .LBE10: .cfi_endproc .LFE113: .size loop, .-loop .global lamp .section .bss .type lamp, @object .size lamp, 2 lamp: .zero 2 .text       :
      :
一応このコードでアセンブリ出力とCのソース場所を確認することができるようになりました。Cのソース場所はディレクティブ.locで表しています.

.loc ファイル番号 行番号 [カラム] [オプション]

コンパイラよりアセンブリ出力は取得できました。が、Cのソースも記載されていないので大きなプログラムではなかなかアセンブリを追うのは難しくなってきます。
Cのソースもアセンブリ上に記載するのが目的でしたがとりあえずここまで。

【方法3】ビルドオプションを変更しアセンブリを出力する

 <<この方法はできませんでした>>
今後この方法の延長線上でできるようになるかもしれませんので、忘備録的に掲載しておきます。

基本的なコンパイルオプションは-S -gです


Arduino IDE ビルドオプションの保存場所
Windows10
    C:/Program Files (x86)/Arduino/hardware/arduino/avr/platform.txt
macOS Sierra 10.12.6
    /Applications/Arduino.app/Contents/Java/hardware/arduino/avr/platform.txt

platform.txt一応バックアップを作ります。
platform_org.txtなどのようにしてコピーします。

ccompiler.c.flagsに-Sを追加します
[platform.txt]
compiler.c.flags=-c -g -S -Os {compiler.warning_flags} -std=gnu11 -ffunction-sections -fdata-sections -MMD -flto -fno-fat-lto-objects
コンパイル時にエラーが出て停止してしまう上に、アセンブリも出力されません。

■ビルドオプション -save-temp を試す

<以下できなかった方法>

[platform.txt]
build.extra_flags=-save-temps

*.s *.iiファイルの保存先
C:\Users\Owner\AppData\Local\VirtualStore\Program Files (x86)\Arduino

ずいぶんと変なところ。C:\Users\Owner\AppData\Local\Arduinoあたりを探しても見つからないわけですね。

GCCのマニュアルより-save-temps

       -save-temps
       -save-temps=cwd
           Store the usual "temporary" intermediate files permanently; place
           them in the current directory and name them based on the source
           file.  Thus, compiling foo.c with -c -save-temps produces files
           foo.i and foo.s, as well as foo.o.  This creates a preprocessed
           foo.i output file even though the compiler now normally uses an
           integrated preprocessor.

           When used in combination with the -x command-line option,
           -save-temps is sensible enough to avoid over writing an input
           source file with the same extension as an intermediate file.  The
           corresponding intermediate file may be obtained by renaming the
           source file before using -save-temps.

           If you invoke GCC in parallel, compiling several different source
           files that share a common base name in different subdirectories or
           the same source file compiled for multiple output destinations, it
           is likely that the different parallel compilers will interfere with
           each other, and overwrite the temporary files.  For instance:

                   gcc -save-temps -o outdir1/foo.o indir1/foo.c&
                   gcc -save-temps -o outdir2/foo.o indir2/foo.c&

           may result in foo.i and foo.o being written to simultaneously by
           both compilers.

       -save-temps=obj
           Store the usual "temporary" intermediate files permanently.  If the
           -o option is used, the temporary files are based on the object
           file.  If the -o option is not used, the -save-temps=obj switch
           behaves like -save-temps.

           For example:

                   gcc -save-temps=obj -c foo.c
                   gcc -save-temps=obj -c bar.c -o dir/xbar.o
                   gcc -save-temps=obj foobar.c -o dir2/yfoobar

           creates foo.i, foo.s, dir/xbar.i, dir/xbar.s, dir2/yfoobar.i,
           dir2/yfoobar.s, and dir2/yfoobar.o.

実際に得た出力結果は以下の通り。抜粋。
[echo.ino.s]
        .file   "echo.ino.cpp"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
 ;  GNU C++11 (GCC) version 5.4.0 (avr)
 ;      compiled by GNU C version 4.8.2, GMP version 5.0.2, MPFR version 3.0.0, MPC version 0.9
  :
 .text .Ltext0: .section .gnu.lto_.profile.791a772d,"",@progbits .string "x\234ca`d`e`" .string "\222\255\214:" .string "\002;" .ascii "\276" .text .section .gnu.lto_.icf.791a772d,"",@progbits .string "x\234ca`d" .string "\0015\006\004`c\330\324q\373\034\033\343\277\231{\01613\335i\371\271\206\227e\313\263\377\247\331Y75,\275\313\315\266\343\314\214\305l\f" .ascii "ma\022F" .text .section .gnu.lto_.jmpfuncs.791a772d,"",@progbits .ascii "x\234}\216=\n\0021\020\205\347M\262Y\020\004\017`\341!\254\275" .ascii "\327VI\263\250\327\020\304\365\027,,\002v6\202\336@;\261\263" .ascii "\260\260]0\223u\265Z_\223\341\315\373^F\023H4\250\236\250\204"    :

.string "j\001"
.string ""
.string "digitalWrite"
.string ""
.ascii "\002"
.string ""
.string ""
.string ""
.string ""
.string ""
.string ""
.string ""
.string ""
.string ""
.string "l\001"
.string ""
.string "_ZN14HardwareSerial9availableEv"
.string ""
.ascii "\002"
.string ""
.string ""
.string ""

ってな結果で。アセンブリは1行たりともでてこない。アセンブリはどこいった?www

[platform.txt]
build.extra_flags=-save-temps -fverbose-asm

-fverbose-asm ってオプションもあるので、こちらも同時に指定してみた。

GCCマニュアルより -fverbose-asm
       -fverbose-asm
           Put extra commentary information in the generated assembly code to
           make it more readable.  This option is generally only of use to
           those who actually need to read the generated assembly code
           (perhaps while debugging the compiler itself).

           -fno-verbose-asm, the default, causes the extra information to be
           omitted and is useful when comparing two assembler files.

           The added comments include:

           *   information on the compiler version and command-line options,

           *   the source code lines associated with the assembly
               instructions, in the form FILENAME:LINENUMBER:CONTENT OF LINE,

           *   hints on which high-level expressions correspond to the various
               assembly instruction operands.

           For example, given this C source file:

                   int test (int n)
                   {
                     int i;
                     int total = 0;

                     for (i = 0; i < n; i++)
                       total += i * i;

                     return total;
                   }

           compiling to (x86_64) assembly via -S and emitting the result
           direct to stdout via -o -

                   gcc -S test.c -fverbose-asm -Os -o -

           gives output similar to this:

                           .file   "test.c"
                   # GNU C11 (GCC) version 7.0.0 20160809 (experimental) (x86_64-pc-linux-gnu)
                     [...snip...]
                   # options passed:
                     [...snip...]

                           .text
                           .globl  test
                           .type   test, @function
                   test:
                   .LFB0:
                           .cfi_startproc
                   # test.c:4:   int total = 0;
                           xorl    %eax, %eax      # <retval>
                   # test.c:6:   for (i = 0; i < n; i++)
                           xorl    %edx, %edx      # i
                   .L2:
                   # test.c:6:   for (i = 0; i < n; i++)
                           cmpl    %edi, %edx      # n, i
                           jge     .L5     #,
                   # test.c:7:     total += i * i;
                           movl    %edx, %ecx      # i, tmp92
                           imull   %edx, %ecx      # i, tmp92
                   # test.c:6:   for (i = 0; i < n; i++)
                           incl    %edx    # i
                   # test.c:7:     total += i * i;
                           addl    %ecx, %eax      # tmp92, <retval>
                           jmp     .L2     #
                   .L5:
                   # test.c:10: }
                           ret
                           .cfi_endproc
                   .LFE0:
                           .size   test, .-test
                           .ident  "GCC: (GNU) 7.0.0 20160809 (experimental)"
                           .section        .note.GNU-stack,"",@progbits

           The comments are intended for humans rather than machines and hence
           the precise format of the comments is subject to change.

こんな結果がでる予定が。。。変わりませんねぇwww
じゃぁ。。。
コマンドプロンプトで

"C:\\Program Files (x86)\\Arduino\\hardware\\tools\\avr/bin/avr-g++" -S -c -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -flto -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10809 -DARDUINO_AVR_NANO -DARDUINO_ARCH_AVR -save-temps -fverbose-asm "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\cores\\arduino" "-IC:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\variants\\eightanaloginputs" "C:\\Users\\Owner\\Arduino\\build\\sketch\\echo.ino.cpp" -o -

以下標準出力

        .file   "echo.ino.cpp"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
 ;  GNU C++11 (GCC) version 5.4.0 (avr)
 ;      compiled by GNU C version 4.8.2, GMP version 5.0.2, MPFR version 3.0.0, MPC version 0.9
 :
        .text
.Ltext0:
        .section        .gnu.lto_.profile.793b07df,"",@progbits
        .string "x\234ca`d`e`"
        .string "\222\255\214:"
        .string "\002;"
        .ascii  "\276"
        .text
        .section        .gnu.lto_.icf.793b07df,"",@progbits
        .string "x\234ca`d"
        .string "\0015\006\004`c\330\324q\373\034\033\343\277\231{\01613\335i\371\271\206\227e\313\263\377\247\331Y75,\275\313\315\266\343\314\214\305l\f"
        .ascii  "ma\022F"

一緒ですね。。。これどうなってるのかなぁ?

ちなみにCコンパイラーはここ
C:/Program Files (x86)/Arduino/hardware/tools/avr/bin/
    


この出力はなんなんでしょうね。
結局 avr_objdump を使えばできますってことでw