リード・ライトが逆転しないの意味

以前、Load-Link/Store-Conditional(LL/SC)の話を書いた。

予約してメモリを書き換える

同じ変数を複数のコアから書き換える場合に、書換の予約をして、

他のコアがアクセスしていなければ書換を実行するという仕組みである。

他にマルチコアのメモリアクセスにまつわる問題を解決するためにメモリバリアというのがあるらしい。


サンプルプログラムでこんなのがあったんですよね。

long atomic_read(volatile long *ptr){
   long ret=*ptr;
   rmb();
   return ret; } void atomic_write(volatile long *ptr, long val){
   *ptr = val;
   wmb(); }

wmbはwrite memory barrierの略らしいから、

ここで確実にメモリライトされるのだろうとは直感的にわかった。

でも、rmbの意図がよくわからなかった。


前提として、CPUは依存関係のないリード・ライトの順番を入れ換えることがある。

REGX = SETTING_X;
REGX; //dummy read
read = REGY;

REGXにライトしてから、REGYをリードすることを期待しているが、

ダミーリードを挟まないとリード・ライトの順番が入れ替わることがあると。

シングルコアのシステムだとこういう工夫でなんとかできるのだが、

マルチコアのシステムでコア間の整合を取るには不十分だという。


そこでメモリバリアという仕組みがある。

wmbやrmbはメモリバリア命令を呼び出すマクロだが、具体的な内容は後で。

さっきのatomic_read, atomic_writeの用法はこんなのだった。

if(atomic_read(&state)==PENDING){
   something(somedata); }

これでstate変数をリードしてから、条件を満たす場合はsomething変数をリードするという意味になる。

atomic_read関数を使用しない、すなわちrmb()を挟まない場合、

state変数がPENDINGになる以前のsomething変数の値を使ってしまう可能性がある。

atomic_write(&state, PENDING);
IPI_TRIG = ipi_mask;

これでstate変数を他のコアに見えるようにライトしてから、IPI_TRIGへのライトをするという意味になる。

ここで言うIPI_TRIGへのライトは他コアに割り込みを発生させる意味である。

atomic_write関数を使用しない、すなわちwmb()を挟まない場合、

割り込みが飛んできてもstate=PENDINGのライトが反映されていない可能性がある。


で、ARMの場合、rmbとwmb、リード・ライト両方のメモリバリアmbは、

#define mb()         asm volatile("dsb"    : : : "memory")
#define rmb()        asm volatile("dsb ld" : : : "memory")
#define wmb()        asm volatile("dsb st" : : : "memory")

こんなインラインアセンブラで展開されるそうである。

dsb命令の前に重要なのがインラインアセンブラの最後の”memory”である。

破壊されるレジスタの一覧に”memory”と指定しているんですね。

これはコンパイラの最適化対策だそうで、これを挟んで命令の入れ替えが発生しないことを表している。

コンパイラが順番を入れ換えることを防ぐだけなら、これでいいらしい。

#define barrier()   asm volatile("" : : : "memory")

何の機械語もないインラインアセンブラである。

で、コンパイラが順番通り並べても、CPUがリード・ライトの順番を入れ換えてしまうことがあると。

それを防ぐためにdsb命令でリード・ライトの両方あるいは一方の完了まで待つ。


さっきのatomic_writeとatomic_readの使用例の組み合わせは少しおかしくて、

state変数へのライト→IPI_TRIGへのライトの順番は逆転しないが、

atomic_writeより前にあるライト→state変数へのライトは逆転の可能性がある。

atomic_readの使用例はstate変数のリード→somedata変数のリードの逆転を防ぐものなのに、

ライト側で逆転の可能性があるっていうのはどうなんだという話である。

割り込みをうけてatomic_readを実行するならば、ほぼ問題ないんだろうが、

厳密にはatomic_writeの前にwmbを置くべきなのだろう。


なかなかイメージがつかみにくい話ではあるんだけどね。

コア間での同期を考えないといけないシチュエーションではこういうのがあるという話だった。

Blogを書きながら勉強してると、これはおかしいかもと思うところはあれこれ。

そのあたりまた点検しないとな。

SPFとDKIMだけで足りないDMARC

先日、DMARC対応の話を書いた。

送信と受信のDMARC対応

すでにSPFとDKIMに対応していればTXTレコード1つ足すだけだね、

とは書いたものの、実はSPF対応していても、場合によってはDKIM対応していても、

そのままではDMARC対応できないケースというのが存在するという。


メールの送信者にはFromとして表示されるヘッダーFromと、

メールサーバーが認識して不達時の連絡先(Return-Path)として利用されるエンベロープFromがある。

SPFは後者、エンベロープFromのドメインに対するなりすましチェックである。

例えば、Return-Pathが …@risonabank.co.jp なのに、同ドメインのTXTレコードに記載されていないサーバーから届いたらNGとすると。

ただ、一般的なメール受信者はReturn-PathではなくヘッダーFromを見る。

Return-PathとヘッダーFromは必ずしも一致している必要はない。

外部のメール送信サービスを利用する場合に一致しないことは珍しくない。

なのでReturn-Pathには自分が管理する適当なメールアドレスでSPFをPassさせて、

Fromになりすましたい会社や公的機関のメールアドレスを書く手法がありうるとは気づいていた。


DKIMについても似たような問題がある。

DKIMの場合はメールに付ける署名の公開鍵をDNSのTXTレコードに書いて、

署名を検証すればドメイン所有者の認めたサーバーから届いたことがわかる。

では、このドメインとは何なのかというと、DKIM-Signatureヘッダのdレコードに記載されたドメインである。

FromのアドレスのドメインとDKIM-Signatureヘッダのdレコードに書くドメインは当然一致していると思ったが、

外部のメール送信サービスが自社の公開鍵で署名する第三者署名というケースもあるらしい。

DKIMをそういう使い方をする理由はよくわからないのだが、DKIM=Passでもこのような場合はアテにならない。


DMARCはSPFとDKIMの両方がNGのメールの処理を規定するものだが、

DMARCでSPF=Passとなるためには、ヘッダーFromとエンベロープFrom(Return-Path)のドメインが一致してなければならない。

全く同じである必要はなく、標準設定ではサブドメインでもよい。

例えば Return-Pathが xxx@foo.example.com でSPF=Pass、

Fromが yyy@example.com であればDMARCはPassとなる。

DKIMも同様でDKIM-SignatureヘッダのdレコードとFromのドメインが一致していなければPassにならない。

こちらについては第三者署名は無効ですよと捉えればよいと思う。


外部のメール送信サービスを使っている場合には、ここを対応しなければDMARC対応とはならない。

例えば、Amazon SESではこのように対応できると書かれている。

Amazon SES の DMARC 認証プロトコルへの準拠 (AWS)

SPF対応については「カスタムの MAIL FROM ドメインを使用する」という対応をすればよいとある。

これは適当なサブドメインをAmazon SESのReturn-Path用に利用できる様にすると。

すなわち、従来はReturn-Pathを zzzz@amazonses.com としていたのを、

bar.example.com宛のメールはAmazon SESのサーバーで処理する設定をして、

かつ、同サブドメインのSPF設定にAmazon SESのサーバーを記載する。

その上でReturn-Pathを zzz@bar.example.com に変更してもらう。

こうすればFromに @example.com から始まる適当なメールアドレスを使えるようになる。

不達時の通知はAmazon SESで処理できるので従来と使い勝手は変わらない。


DKIM対応については3つの方法があると書かれている。

1つはAmazon SESの公開鍵を自分のドメインの公開鍵として公開する方法である。

これで、DKIM-Signatureヘッダのdレコードにそのドメインを指定して、

Amazon SESで持っている秘密鍵で署名できるようになる。

なるほど、その手があったかという感じである。確かにこれは簡単。

2つ目は自分で用意した鍵ペアの公開鍵をドメインで公開、秘密鍵をAmazon SESに支給する方法である。

僕は最初、この方法しかないと思っていた。もっとも素直な方法である。

3つ目はAmazon SESに送信依頼するメールにあらかじめDKIM-Signatureヘッダを付けておく方法。

この方法はエラーをAmazon SES側で検出できないから注意しろとはあるが、

DKIM-Signatureを任意に設定できるメールサーバーであれば、

メールサーバー側がDKIM対応してなくてもDKIM対応できる仕組みである。


ところでGmailでは大量にメール送信する人にSPFとDKIMの両対応を求めている。

DMARCではどちらか一方だけ対応していればOKなのに、なぜ両対応が必要なのか。

これはおそらく転送メールの都合ではないかと思う。

SPFは送信元のメールサーバーとReturn-Pathの一致を確認するが、

転送メールは他のサーバーから転送されてくるのでこれを満たせない。

一方のDKIMは元々Passしていたメールをそのまま転送すれば当然Passする。

大量送信すれば、そのうち一定割合は転送サービスで転送されてしまうだろう。

そうなったときSPFだけだとPassできなくなってしまう。

だからDKIMも付けて転送メールでもDMARCをPassできるようにしてね。

こういう趣旨なのではないかと思う。


じゃあ、逆にDKIMだけでDMARC対応してしまえという考えもありそうだが。

SPFでDMARC対応するにはReturn-PathとヘッダーFromのドメインを一致させる必要がある。

DKIMの場合はここが一致しなくてもFromとDKIM-Signatureのドメインが一致すればよい。

でも、実際はそういう例はあまり多くないのだろう。

Return-Pathのドメインを合わせるカスタマイズの方が結局は容易なのだろう。


ちなみにヘッダーFromとエンベロープFrom(Return-Path)の不一致については、

Gmailではこれまでも自主的に「xxxxx.xxx経由」という表示をしてきた。

これを表示する条件はDMARCのNG条件に似ていて、

ヘッダーFromとエンベロープFromのドメインが一致しなくて、

かつヘッダーFromのドメインでDKIMをPassしていない場合である。

DMARC対応となればエンベロープFromのドメインを一致させる方向に動いたと思うが、

それ以前はDKIMの導入で「xxxxx.xxx経由」の表示を消す考えもあったかもしれない。


今日、ちょうどSPFがPassでDMARCがFailとなるメールが届いたんですよね。

Return-Path: <amz@shxiudada.com>
DMARC-Filter: OpenDMARC Filter v1.4.2 hdmr.org 9CE2FDBA40
Authentication-Results: hdmr.org; dmarc=fail (p=quarantine dis=none) header.from=mastercard.com
Authentication-Results: hdmr.org; spf=pass smtp.mailfrom=shxiudada.com
From: "MasterCard" <info@mastercard.com>

Authentication-Resultを見るとspf=passとなっているが、

同ヘッダーには検証されたドメインの名前が書いてあるがヘッダーFromとはまるで一致しない。

なので、DMARCの判定においてはこの結果は利用しないということになる。

DKIMについてはそもそも署名なし。

その上でヘッダーFromのmastercard.comのDMARCポリシーは存在しているので、

DMARCの判定をして、SPF無効・DKIMなしで dmarc=failと判定。

p=quarantineと記載されているので、迷惑メールとして扱うのが相当とわかる。

導入早々わかりやすい例がきてDMARCの効果を確認出来た。

送信と受信のDMARC対応

Y!mobileからMMSでDMARCによるなりすましメールの対策を導入すると来ていて、

これ自体は特に問題はないが、そういえば自分のメールサーバーはどうだっけと。

Yahoo!メールヘルプ/DMARCとは (Yahoo!)

すでにSPFもDKIMも導入しているから設定を加えればOKですね。


そもそもSPFとDKIMも含めたわかりやすい説明はここに書いてある。

ドメイン認証技術「DMARC」について (Yahoo!)

DMARCはSPF, DKIMの判定でNGとなったメールの処理方法を決めることを主目的としている。

だから、基礎としてSPFまたはDKIMを導入していれば、処理方法を記載するだけでよい。

SPFとDKIMで迷惑メールよけ

SPFは送信するメールサーバーをDNSのTXTレコードに記載する方法。

MXレコードのサーバー以外から届いた場合は怪しい(softfail)という記載例を書いている。

特に送り側のメールサーバーには細工がいらないのでかなり普及している。

DKIMは電子署名を付けて送信し、DNSのTXTレコードに記載した公開鍵で検証できるようにする。


今までこれらの技術でNGとなったメールの扱いは不明瞭なところがあった。

DKIMについてはADSPというNG時の動作を決める仕組みがあったが、廃止されている。

で、Gmailに大量送信するメールサーバーについては、DMARCへの対応が要求されている。

大手の送信ドメイン認証「DMARC」導入率が8割超に、Gmailのガイドラインが奏功 (日経XTECH)

SPFとDKIMの両方を導入した上でDMARCに対応することが求められている。


というわけで、僕の場合は、DNSに下記の設定をするだけで済む。

_dmarc                  TXT     "v=DMARC1; p=reject"

p=rejectはSPFもDKIMもダメなら拒否してねという意味になる。

すでにSPF, DKIMともに実績が十分あるのでこの設定だが、rejectはあまり一般的ではないかも。

Yahoo!メールのヘルプにも書いてあるのだが、p=quarantineで迷惑メールに分類の意味で、これが一般的である。

p=noneにするとそのまま受信してという意味にはなるのだが、

その場合はなりすまし対策に弱いドメインとみなされ、結果として迷惑メールに分類されやすくなる場合があるという。

そういう意味では最低でもSPFは導入した上で、p=quarantineでDMARCを導入するとよさそうだ。


DMARCにはもう1つ目的があって、それがレポート機能である。

受信サーバー側で当該ドメインからのメールの判定結果を送ってくれる。

上記の設定に rua=mailto:… を書き加えると、統計データが送られてくる。

DMARCでは設定必須なのかと思ったのだが、必須というわけではない。

送られてきても使い道がないと思えば書かなくてもよい。

ただ、これを受け取ることでなりすましメールの実情を知ることが出来る。


さて、これでGmailとかYahoo!メールに送って届いたメールのヘッダを見る。

Authentication-Results: mx.google.com;
        dkim=pass header.i=@hdmr.org header.s=xxxx header.b=XXXXXXX;
        spf=pass (google.com: domain of h@hdmr.org designates xxx.xxx.xxx.xxx as permitted sender) smtp.mailfrom=xxxxx@hdmr.org;
        dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=hdmr.org

dkim=pass, spf=pass, dmarc=pass ということで合格ですね。

DMARCの設定をする以前はdmarc=passが付かなかったはず。


逆に受信側なのだが、OpenDMARCを導入してみた。

もともとpolicyd-spfを導入していたのを、OpenDMARCでSPFとDMARCの判定をする形に改めた。

なお、OpenDKIMについてはDKIMの署名・判定のために併用する。

送信メールについてはOpenDKIMで署名→OpenDMARCでは何もせず、

受信メールについてはOpenDKIMで署名検証→OpenDMARCではSPF判定、DKIM・SPFの結果を総合してDMARCの結果を記載という流れになる。


OpenDMARCは/etc/opendmarc.confで設定するが、

IgnoreMailFrom hdmr.org
RejectFailures false
SPFIgnoreResults true
SPFSelfValidate true

IgnoreMailFromで自分のドメインを書かないと、これから送信するメールを検証してしまう。

RejectFailuresは標準でfalseだが念のため。こう書くとp=rejectでも受信する。

後で書くのだが端的に言えばDMARCには期待していないということである。

SPFIgnoreResults, SPFSelfValidateはOpenDMARCでSPFの検証を行うための設定。

これをした上で従来のpolicyd-spfの設定を削除することとする。


で、これをPostfixに設定するわけだが、OpenDKIM→OpenDMARCの順にmiltersに記載すればよい。

smtpd_milters = unix:/run/opendkim/opendkim.sock,
unix:/run/opendmarc/opendmarc.sock
non_smtpd_milters = $smtpd_milters

ただ、Permission deniedのエラーが出てしまう。

これはpostfixユーザーにsockファイルのアクセス権がないのが原因で、

opendkimグループとopendmarcグループにpostfixユーザーを参加させればよい。

# usermod -aG opendkim postfix
# usermod -aG opendmarc postfix

これでDMARCの判定が適切に行われる。

DMARC-Filter: OpenDMARC Filter v1.4.2 hdmr.org 1BBC5DBA40
Authentication-Results: hdmr.org; dmarc=pass (p=none dis=none) header.from=gmail.com
Authentication-Results: hdmr.org; spf=pass smtp.mailfrom=gmail.com
DKIM-Filter: OpenDKIM Filter v2.11.0 hdmr.org 1BBC5DBA40
Authentication-Results: hdmr.org;
     dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=....

3つのAuthentication-Resultsヘッダで書かれるんだな。

このAuthentication-Resultsのdmarc=pass または dmarc=fail を見て、

マーキングやフォルダ振り分けなど行うことが出来るわけである。

とりあえずはThunderbirdでdmarcの結果によってタグを付けるようにした。

(元々SPFとDKIMの結果でタグを付けるようにしていたのを変更した)


ただ、これが迷惑メール対策として効果的なのかはよくわからない。

というのもSPFもDKIMも他人のドメインへのなりすまし対策であって、

ドメインをなりすました迷惑メールというのはそう多くない印象である。

昔はSPFもDKIMもNGなら迷惑メールに分類する対策をしていたが、

迷惑メールでもない誤設定のためかメールがNGになる割には、迷惑メールがOKとなることも多く不便だった。

このためマーキングに留めるようにしたのである。

最近はGmailがDMARC対応を求めたためかNGになるメールは減りましたが。


なぜあまり効果的ではないのかという話だが、ドメインのなりすましがそう多くないからである。

例えば、最近は りそな銀行 へのなりすましメールが何通か届いたが、

risonabank.co.jpから送信したように見せようとはしてないんですよね。

自分で取得した適当なドメインにSPF・DKIM・DMARCの設定をした上で送ってきているのである。

そしたらそのドメインでSPF・DKIM・DMARCが全てPASSになってしまう。

確かにドメインをりそな銀行に偽装はしていないのだが、

タイトルや本文でりそな銀行から送ったように見せていれば、それは立派ななりすましメールですよね。

こういうことがあるのでメールは注意して見なければならないのだが、

結果としてSPF・DKIM・DMARCはこのタイプのなりすましメールの対策にはなっていないのである。


各所で迷惑メール対策が進む中で、行儀のよい迷惑メールが増えた印象で、

こうするとなかなか正当なメールとの識別が難しいのが実情である。

Gmailが大量にメールを送るところにDMARC対応を求めたのは、

迷惑メールの報告が少なく信頼できるドメインを明確にするためなのではないか。

  • xxx.exampleからのメールは迷惑メールの報告が少ないがDMARC非対応
  • yyy.exampleからのメールは迷惑メールの報告が多いがDMARC対応
  • zzz.exampleからのメールは迷惑メールの報告が少なくDMARC対応

yyy.exampleのような迷惑メールの報告が多いドメインは、DMARCの有無によらず迷惑メールへの分類を進める。

xxx.exampleは今は迷惑メールの報告は少ないが、DMARC非対応だとなりすましが容易である。

すなわち迷惑メールの温床になる危険があるのでやめてくれと言っている。

zzz.exampleのような迷惑メールの報告が少なくDMARC対応となっていれば、

DMARCがpassであれば迷惑メールの可能性は低く信頼度が高い。

DMARCがfailになれば なりすまし で迷惑メールと判断できる。


Gmailのような大量のメールが届く事業者ならこういう対策もあるだろうけど、

ビッグデータなしにDMARCを迷惑メール対策に使うのは難しいのかなと。

DMARCがfailする迷惑メールがたくさん届くなら考えるんだけど、全然来ませんからね。

それでも何か役に立つかもしれないとマーキングだけはしていると。

wfi命令で割り込みを待つ意味

今まであまり使うことがなかったのがマイコンのスリープ機能である。

低消費電力化が求められるシステムなら当然使うんだろうけど。

割り込みが来るまで処理を止めるという話ですね。


で、ARMの場合、wfi命令というのが用意されている。

この命令を実行すれば割り込みが来るまでお休みになる。

while(1){
   if(flag==0){
     __asm("wfi");
   }
   if(flag==1){
    ...

割り込み処理でflag変数に何か入るまでループで待つというのを、

flag変数が0ならばwfi命令を実行して、割り込みが入るまでスリープ状体でお休みと。

wfi命令とif(flag==1)の間で割り込みが実行されることを期待しているわけだ。


でも、if(flag==0)とwfi命令の間に割り込みが入ったらどうするのだろう?

ここでは省略したのだが、実際のコードではこの間にUARTの受信割り込みを有効化するための処理が入っている。

フラグを見て直後にwfi命令を実行していると言い切れない部分もある。

そんなわけでwfi命令の使用例を調べてみると、こんなアセンブラコードが見つかった。

cpsid i /* 割り込みマスク */
mov r4, #0
msr basepri, r4 /* 全割り込み許可 */ wfi
cpsie i /* 割り込み受付 */

wfi命令は割り込み待ちの命令だが、その直前に割り込みマスクをして、

それでwfiを出たら割り込み受付をするという書き方である。


これを見てもわかるのだが、wfi命令というのは割り込み待ちが発生した時点で解除される。

なので割り込みマスク状態で動かしてもOKなんですね。

むしろそのように書くことでwfi命令に入るまでに割り込みが消化されてしまうリスクがなくなる。

というわけで、さっきのflag==0ならwfiで割り込み待ちというのは、

while(1){
   __asm("cpsid i");
   if(flag==0){
     __asm("wfi");
   }
   __asm("cpsie i");
   if(flag==1){
    ...

と書くとよい。

こうすればif(flag==0)とwfi命令の間に割り込まれる心配がなくなる。


このあたり厳密にやらなくてもだいたい動いてしまうんですけどね。

さっきのアセンブラコードの場合、wfi命令の前に割り込み優先度の操作をしている。

このため割り込み優先度の操作をした途端に割り込まれる可能性がある。

なので、一旦割り込み禁止にして、優先度を変えているんですね。

で、優先度を変えたことで割り込み待ちになればwfi命令は即抜けになる。

割り込みマスクを解除したときに割り込み処理が走るというわけ。


このコードを見る前から割り込みマスク中でもwfi命令から抜けるらしい、

という話は見ていて、一体何のためにそうなってるんだろうと思ったら、

こういう使い方をするためだったんですね。

もちろん割り込み有効状態でwfi命令を使っても、それはそれで動くのだが、

割り込みマスク状態で使って、wfi命令を抜けたら割り込みマスクを解除するという動きの方がより正確な動きのようである。


あと、これは特に関係ないが、wfi命令を検索するとwfe命令のことがひっかかる。

割り込みが発生するまでスリープするという目的ではwfi命令を使うのだが、

wfe命令というのは他のコアでsev命令が実行されるまで待つことを目的としている。

wfeは”Wait For Event”、sevは”Set Event”で一対の命令である。

ある変数が書き換わるまでポーリングするという処理をするのに、

wfe命令で待って、他コアで書き換えたらsev命令を実行するような使い方をするらしい。

    sevl
1:  wfe
    ldr r0, [r1]
    cbz r0, 1b

sevl命令は他コアでsev命令が実行されてなくても、次のwfe命令は抜けるという意味になる。

(sevlは”Set Event Locally”の意味で、自コアだけイベントをセットする意味)

1回はr1レジスタのアドレスに格納されたフラグをチェックして、

そのフラグが0ならばwfe命令に戻って待つという記述である。

で、この間に割り込みが入るか、他コアでsev命令が実行されればwfe命令を抜けると。

割り込みが入るまで待つという部分はwfi命令とも共通するのだが、使用目的が異なる。

他コアからのフラグをポーリングしている間でも割り込みは入らないと困るから抜けるだけである。

これは使わなさそうな感じがするが、そういうのあるという余談である。

予約してメモリを書き換える

マルチコアの制御を行うプログラムでcmpxchgという関数があった。

この関数でプログラムが停止して、いろいろ面倒なことがあった。

アセンブラで書かれている関数でよくわからないなと思って調べたら、マルチコアのシステム特有の処理らしい。


この関数はメモリ上の値がある値であることを確認したら書き換えるというものである。

Compare and Exchange の略で cmpxchg という名前だったらしい。

で、ARMのA32命令で書くとこんな感じの関数である。

# 引数: r1=ptr, r2=old, r3=new
# 返り値: r0
1:  ldrex   r0, [r1]
    mov     r4, #0
    teq     r0, r2
    strexeq r4, r3, [r1]
    teq     r4, #0
    bne     1b

ldrex命令を実行してアドレスr1の値を取得してr0レジスタに格納する。

ただのロード命令と違うのは、書換の予約をするということである。

r2レジスタには当該アドレスで想定されている値が格納されている。

ldrex命令で取得した値と不一致ならば、書換をせずに終わり。

一致していれば、r3レジスタに格納された値への書換を試みる。

それがstrex命令(eqを付けてr0=r2の場合のみ実行としている)である。

strex命令はアドレスr1の値がldrexでの予約後に他のコアが触っていないか見て、

触っていなければr3への書換を実行してr4に0を格納する。

もし触っていれば書き換えずにr4に1を格納し、その場合はldrexからやり直す。

ちなみに bne命令の分岐先1bというのは上にあるラベル1という意味らしい。

逆に下にあるラベル2は2fと書ける。そういうアセンブラの記法があると。


この方式を Load-Link/Store-Conditional(LL/SC) と呼ぶそうである。

Load-Linkが読み出して予約するというのでldrex命令に相当する。

Store-Conditionalが予約後に書換がない場合に書き込むというのでstrex命令に相当する。

そもそもこのような方法が必要になるのはマルチコアだからである。

シングルコアであれば割り込み禁止にしておけば、他のプログラムに書き換えられることはない。

でも、マルチコアの場合、他のコアが書き換える可能性がある。

打開策はいくつかあるが、LL/SCは予約先への他のコアのアクセスを監視することで解決する方法である。

もしも予約先に他のコアがアクセスしてたら書き換えずにやり直すと。


このcmpxchg関数の使用例として、こんなものがある。

if(cmpxchg(&lottery,0,1)==0){
   something(); }

lotteryの初期値が0であるのが前提だが、

最初に実行したコアが0→1の書換をしてsomething()に書いた処理を実行し、

それ以降に実行したコアはすでに1なので何もしないと。

めったに競合しないとは思うが、もし競合すればcmpxchg関数の中でやり直すことになる。


この方式は予約したアドレスへの他のコアのアクセス有無がわかればよいが、

この仕組みが働く領域と働かない領域があるようで、

その都合でcmpxchg関数でプログラムが停止する問題が起きていた。

他のコアのアクセスを監視するというところに何らかの制約があったよう。

シンプルそうに見えるのだが、意外に難しい仕組みである。

2つのマイコンを1つにする難しさ

少し前からマイコンについてのあれこれを書いてきたのだが、

これは仕事でとあるシステムを構築するための検討に関連しての話である。

これぐらいなら書いてもいいんじゃないかということで、どんなものを作ろうとしているかという話。


ことはシステムに搭載された2つのマイコンが製造中止になる話から始まる。

けっこう古いマイコンだったんですね。

単純に新しいマイコンに置き換えられればよかったのだが困難な事情もあり、

元々搭載されていたFPGAをマイコン入りのFPGAに置き換えることになった。

FPGAにマイコンの周辺回路を作り込めるのがポイントだったんですね。

で、このマイコン入りFPGAには複数のコアが搭載されているので、

2つのマイコンの役割をコア1つずつ割りあてようとした。


発想としてはありそうな話なのだが、これがそう簡単なことではない。

というのもこの手のマルチコアのプロセッサーはコアを個別にリセット・起動することは想定されていないからである。

使い方の例として、複数のコアを使ってLinuxを立ち上げる例や、

コアの一部をLinux、一部をリアルタイムOSで使う例が書かれているのだが、

外部信号で個別に起動・停止を行うハードウェアの仕組みは備わっていない。


マイコンが変わることでリアルタイムOSも新しいのを買わないといけない。

それで問い合わせたところ、全コアまとめて1つのOSでマネージメントした上で、

タスク群Aはコア1に、タスク群Bはコア2に割り付けて動作させるという構成を提案された。

元々複数のマイコンでやってた仕事を1つのマルチコアシステムにまとめるのはよくある話なんだろう。

タスク群ごとにコアを分けておけば、他方のタスク群の処理時間が増えても影響を受けずに済む。

全コアまとめてOSに渡せば、そこからはOSで対応してくれるし、

コア間の通信はOSの一般的なシステムコールで対応できるのもよい。


ただ……このシステムは元々2つのマイコンに分かれている前提の要素が多すぎた。

個別での起動・停止、個別でのファームウェアアップデートなどがある。

個別の起動・停止をタスクの生成・破棄で代替出来ないか。

個別でのファームウェアアップデートをアプリケーション部のみの差し替えで対応できないか。

1つのOSで全コアをマネージメントする方式でどこまで互換性を保てるか。


一方でコアごとにOSを分けるという構成もあるはずじゃないかと。

Linuxを動かすコアと、リアルタイムOSを動かすコアに分けられるなら、

コアを分けて2つのリアルタイムOSを起動させることはできないか。

これ、起動させるだけならば、そこまで難しいことではない。

問題は個別に起動・停止を行う仕組みである。

ウォームリセットとコールドリセット

これはマイコンにもよると思うのだが……

ここで書いたソフトウェアから見たウォームリセット、リセットハンドラへのジャンプぐらいしかない。

いくつか候補にあったマイコンも見たけど、ほぼこれしかないね。

(1つは他のコアからのレジスタ操作でハードリセットできそうにも見えたが)


一方でこの方法でのリセットができないシステムはないだろう。

割り込みを受けてリセットハンドラへのジャンプというのは誰でもできる。

この方法でどれぐらい完全なリセットになるかという問題はあるが、

OSを立ち上げなおすという動作は概ね実現できるように思える。

個別で停止して、個別にアップデートして、個別に起動する。

起動部分を多少注意して作れば実現できなくはない話である。


もっともOSを分ける構成でも全コアで共通的な機能は分けることができない。

このためコアごとにOSを分けることにどれぐらいメリットがあるかという指摘はけっこうある。

多少なりともブートローダーやOSに手を加える部分が増えるのは事実。

とはいえOSを1つにする場合にはアプリケーション側での工夫が多くなるので、

どちらの方が容易かという話なんだろうなという気もする。


というわけでマルチコアのプロセッサーに複数のマイコンをまとめるのは、

発想としてはありそうだけど、実際には簡単じゃないねという話だった。

他にもいろいろ難題があるんですけどね。Blogに書ける愚痴はこれぐらいで。

引数を渡せる割り込み

関東平野は頻度は低いが雪になると相当に降るというので、

今日の帰り道はけっこうな雪の中を歩いて帰ってきた。

今日、特に午後は在宅勤務の人が多かったので、ガラガラだったが。

滑りにくい靴とか、防寒具とか用意した上で、通勤経路を勘案して問題ないということで、出勤してたわけだけどね。


CPUには割り込みを発生させる命令というのがある。

ARMだとSVC命令だし、x86だとINT命令ですか。

で、最近知ったのだが、これらの命令を使ってシステムコールを発生させるとき、

レジスタに値をセットして渡したり、返り値を受け取ったりすることがあると。


一般的な割り込みは割り込み前の汎用レジスタの状態に依存せず動いて、

割り込み前後で汎用レジスタの値は変えないように退避する。

でも、システムコールの場合はそうではない場合があると。

引数をレジスタで受け取って処理を変えることもあるし、

返り値をレジスタに入れて返すということもあると。

SVC命令もINT命令も割り込みの番号を設定することができるけど、

どうせレジスタで引数を渡すなら、命令に付ける番号を使わないという実装もあるよう。


こういうことができるのは、他の割り込みと違って起きるタイミングが決まってるからこそだよね。

割り込みというよりは、関数呼び出しに近い使われ方をしてるんですね。

それでも割り込みという仕組みを使うのは、特権モードになるためである。

ユーザーモードから特権モードに遷移するには割り込みを起こす必要がある。

そのためにソフトウェア的に割り込みを起こす方法を用意していて、

その使われ方というのは関数呼び出しと近い部分があるというわけである。


今まで全て特権モードで動くようなシステムばっかりだったので、

わざわざこういう仕組みを使うことはなかったというのが大きいですけどね。

そうか、ユーザーモードから見れば関数呼び出しみたいなものなのかと。

そんなわけで今さら気づいた話だった。

確かに他の割り込みとは全然違いますよね。

割り込み信号の作り方

マイコン入りのFPGAの設計についていろいろ話をしていて、

FPGAのロジックからマイコンに割り込み信号を入れようと思うが、

果たしてどういう設計にするのがよいかという話をしていた。


割り込み信号は大きくレベルトリガとエッジトリガの2方式がある。

レベルトリガは、例えば割り込み信号がHiであるうちは割り込みが発生し続けるというもの。

通常は割り込み処理の中で割り込み要因を消す処理を行う。

エッジトリガは、割り込み信号の立ち上がり・立ち下がりで割り込みが発生するもの。

こちらは一度割り込みが発生すると、次に信号が変化するまで割り込みは発生しない。


想定していた ロジック部→マイコン部 の割り込み信号について、

担当者はエッジトリガだと思っていたみたいなのだが、

テストプログラムを書いてみるとレベルトリガであることが判明した。

ロジック部→マイコン部の接続方法によってはエッジトリガも可能なのだが、

おそらくレベルトリガを前提とした構成になるのではないかと言っている。

  1. FPGA部で割り込み要因を検出する
  2. レジスタの割り込み要因のビットを1にして、割り込み信号をHiにする
  3. マイコンの割り込みハンドラで要因レジスタをリードする
  4. 要因レジスタの確認したビットをクリアする
    (全ての割り込み要因がクリアされれば割り込み信号はLoになる)
  5. 要因レジスタにリード結果に応じて処理を行う
  6. (未確認の割り込み要因が残っていれば再度割り込みが入る)

実際のところ、エッジトリガとしても似たような構成になるんだよな。

割り込みコントローラでエッジを検出して、コアに割り込みを発生させ、

割り込みハンドラでは、割り込みコントローラから割り込み番号を取得して、

その番号の割り込みを消化したことを割り込みコントローラに伝え……

やってることは上と同じですよね。

マイコンで持っている割り込みコントローラでやるか、マイコン部の外側でやるかの違いで、

マイコン入りFPGAなので、こういう処理をFPGA部に作り込めるって話。


わりとマイコン内部ではレベルトリガの割り込みが多いんですよね。

そちらの方が取扱が便利だからだと思うけど。

一方で外部ピンからの割り込みはエッジトリガになることが多いかな。

外部からの入力信号が変化したときに割り込みが欲しいですからね。

FPGAのロジック部から与えられる割り込み信号の中には、

元々は別のICからエッジトリガで入っていたものもあるので、

それをロジック部でレベルトリガに改めるのか、割り込みコントローラにエッジトリガとして処理させるのか。

ここは意見が分かれるかも知れませんけどね。

ECC付きメモリを先読みして停止

先週末からマイコンでテスト用のプログラムを動かしていたら、

変なところでプログラムが停止するから困っていたのだが、

ECC付きメモリの初期化されていない領域を踏んでいたのが原因だった。

それだけならここまで苦労はしなかったような気はするが。


ECCって誤り訂正符号のことね。

1ビットのデータ誤りは訂正できて、2ビット誤りは検出できるなど。

何らかの理由でメモリが化けても訂正で回復できることに期待しているが、

注意しなければならないのは、あらかじめ誤り訂正符号を付けたデータが書き込まれてなければならないこと。

起動時にはランダムなデータがメモリに格納されているので、

この状況でデータを読み出すと異常データだらけになってしまう。


ECC付きメモリを使っていることは実はあまり意識していなかったのだが、

一方でメモリの使用する領域はあらかじめ何らか書き込んでいるつもりでいた。

誤り訂正符号が正しく書き込まれていない領域はアクセスしないはずだと。

未使用領域へのアクセスを指示していたとすれば、例外割り込み時の情報から容易に推測できたはずである。


今回の問題が発生した原因はメモリの先読みによるものだった。

どういうアルゴリズムで先読みしているのかはよくわからないのだが、

プログラムカウンタが一定のアドレスに到達すると、

そこからいくらか先のアドレスへのリードアクセスが発生しているようだ。

この先読み動作でキャッシュへのデータ取り込みを行うのだろう。

とはいえ、先読みしたデータが必要とは限らないし、有効なデータが入っているとも限らない。

この先読み先のアドレスは未使用領域なので初期化してなかったんですね。


というわけでメモリの全領域を初期化する処理を書き加えた。

サンプルを参考にスタック領域を初期化するプログラムをアセンブラで書いていたのだが、

スタックに限らず、メモリの全領域を初期化するように作り替えた。

スタック領域以外は後のC言語で書いてるプログラムで初期化してもよいが、

スタック領域とそれ以外に分けて対応するより容易だったのでそれで。

初期化の効率を考えればまた違った考えもあるかも知れないけど。

この修正でとりあえずプログラムが停止する問題は回避できるようになった。

(他にも不可解な挙動はあるけど、停止はしなくなった)


サンプルプログラムではこのあたり全然ケアされてない気がしたが、

セクション構成によってはこういう問題が発生しなかったのかも知れない。

すなわち命令の先読み先が何らかのデータで初期化されていればよいので、

命令領域の後ろにある程度の変数領域・スタック領域があれば、この問題は回避できる可能性がある。

確かにセクション構成を大きく変更したときにこの問題が顕在化している。


以前、ECC付きメモリの初期化プログラムを書いたことがあって、

そのときの経験も多少は生きているのだが、それでも想定外だったなぁ。

早いこと全面初期化しておくに越したことはないのだが、

そのためのプログラムはユーザーで書かないといけないんですよね。

答えがわかってしまえば大した話ではないんだけどさ。

YMODEMを使う

マイコンのメモリ上にプログラムデータを書き込むのはICEからやればよいと思ったが、

どうにも思ったようにいかない部分があって、別の転送方法を考えることに。

それで昔、TeraTermからデータ転送する方法を見たことあるなと思ったら、

そのマイコンのサンプルコードにYMODEMのコードがあった。

なるほど。YMODEMか。と使ってみることに。


YMODEMというのはシリアル通信などでバイナリデータを転送する方式の1つである。

元々XMODEMというのがあって、それの改良版がYMODEMだと。

で、さらなる改良版にはZMODEMがあるが、かなり複雑な仕組みである。

デバッグ用のコンソールではYMODEMぐらいが使いやすいのかも知れない。

で、サンプルコードから移植すると「C」という文字が表示されて、なんだこれと。


というわけでYMODEMというのはどういう方式かということである。

受信側は準備が整えば「C」という文字を送る。

これを見た送信側はブロック0としてファイル名やデータサイズを送信する。

1ブロックで128バイトまたは1024バイトのデータを転送できて、

これにヘッダとCRC16が付いているという。

で、データの受信が完了すると受信側はACK(06H)を返す。

続いてブロック1以降でデータ本体を送信して、受信側はACKを返すを繰り返すが、

ブロック1だけは受信側からの「C」を待つよう。(XMODEMとの互換性のため?)

全ブロックの送信を完了したら、送信側はEOT(04H)を送る。

これに対してACKではなく、NAK(15H:通常時に送ると再送要求になる)を返すと、

2個以上のファイルを連続受信できるが、サンプルコードではファイル1個で終わるということで、ACKを返していた。

YMODEMの本来の仕様とは異なるが、XMODEMとの互換性で通じるようだ。


ところでXMODEMに対するYMODEMの改良点なのだが、

1つ目はブロック0の存在で、ファイル名も伝達できるという特徴がある。

プログラムデータのファイル名は今回は特に必要ない要素だが。

ただ、ファイルサイズをブロック0で転送できることは活用している。

2つ目はブロックのチェックサムがCRC16であることと、1024バイトのブロックが使用できること。

受信側から送る「C」はCRCをチェックに使うという意味を表してるらしい。

3つ目は複数ファイルの連続転送ができること。これも今回は使ってない。


ただ、これがなかなかうまくいかなかったんだな。

ぶっちゃけサンプルプログラムの出来がよくなかった。

タイムアウトになってもTeraTermはYMODEMに占有されっぱなしで、

なんとかならんのかと調べるとCAN(18H)を2個送ると、中断の意味になるので、とりあえずこれを送りつけたり、

TeraTermでYMODEMのログを出力できる機能があるのでこれを使ったり。

そうやって掘っていくと、ブロック0の後に「C」を送信する部分がうまく作れていなかったり、

そんな修正をしてなんとか使えるようになった。


で、YMODEMの動作説明を見てもわかるが、受信側が送信を要求するという構造なので、

シリアルポートからマイコン側に受信開始を要求すると「C」が届く。

この受信要求を数秒周期で何度か繰り返すように作っておく。

で、この間にTeraTermをYMODEMの送信モードにして「C」が受信できたところからデータ転送が始まるというわけである。

データサイズは大きくないので、115200bpsですぐに転送は終わる。


という風な使い方をするわけですね。

バイナリデータを転送する方法はいろいろ考えられるわけだけど、

TeraTermでそのまま使えるという点でデバッグには便利な仕組みである。

実際のシステムでは使わないけど、実験用には好都合だね。

YMODEMさえ動けば、後は思ったように動いたのでよかったんだけどね。

もうちょっとまともなサンプルコードだったらなぁ……

デバッグ用にサクッと使えるとうれしかったんだけどね。