ARM64 で非キャッシュ領域に memset() を実行するとバスエラーになる件


# はじめに


ZynqMP(ARM64) で PL 部とデータのやりとりをする際、 uio やudmabuf を使って mmap() でユーザー空間にアクセス領域を確保することがあります。uio はPL部のレジスタ空間をユーザー空間にマッピングすることが多く、そのために通常は非キャッシュ領域になります。

こうしてユーザー空間にマッピングした非キャッシュ領域に対して、libc の memset() 関数を使ってメモリを初期化すると、バスエラーが起きることがあります。

この記事ではバスエラーが起きるメカニズムを説明します。



glibc の ARM64(aarch64)用の memset() のソースコードは次のURLを参考にしました。この記事の最後にもソースコードを載せていますので参考にしてください。みてのとおり、バリバリのアセンブリ言語で書かれています。

*  https://code.woboq.org/userspace/glibc/sysdeps/aarch64/memset.S.html


# バスエラーが起きるメカニズム


非キャッシュ領域でmemset() を実行するとバスエラーになる要因は、実は２つあります。一つ目は データキャッシュクリア 命令によるメモリクリアを使っていること、二つ目は非アライメントなストア命令を使っていることです。




## データキャッシュクリア命令のよるメモリクリア


例えば次の例のように、セットする値が0で、ある程度セットするバイト数が多い場合、ARM64 用のmemset() は データキャッシュクリア命令(dc zva)を使ってメモリクリアを行うことがあります。


```C:sample.c
		regs = (uint64_t*)mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0);
		memset((void*)regs, 0, 0x1000);
```




dc zva 命令は ARM64 独自の命令です。dc はデータキャッシュ操作命令、zva はデータキャッシュ操作のうち、指定されたアドレスを含むデータキャッシュをゼロクリアすることを示します。この命令は、その名のとおり、データキャッシュを操作するためのものです。当然、非キャッシュ領域に対してこの命令を実行するとエラーになります。


## 非アライメントなストア命令


ARM64 用の memset.S には、よく次のような dstend から負方向にオフセットされたアドレスにデータをストアする命令が見受けられます。


```C
ENTRY_ALIGN (MEMSET, 6)
	DELOUSE (0)
	DELOUSE (2)
	dup		v0.16B, valw
	add		dstend, dstin, count
	cmp		count, 96
     (中略)
	str		val, [dstend, -8]
     (中略)
	str		valw, [dstend, -4]
     (中略) 
	strh		valw, [dstend, -2]
     (中略) 
	str		q0, [dstend, -16]
     (中略) 
	str		q0, [dstend, -32]
     (中略) 
	stp		q0, q0, [dstend, -32]
     (中略) 
2:	stp		q0, q0, [dstend, -64]
	stp		q0, q0, [dstend, -32]

```


dstend は 領域の先頭アドレス(dstin)にセットするサイズ(count)を足した値、つまり最後のアドレス＋１を示しています。先頭アドレスと転送サイズの組み合わせにより、dstend は非アライメントなアドレスを指すことがあります。

例えば、memset(0x1000, 0xCC, 3) のように３バイト分セットする場合、下の図のように命令が実行されます。


![Fig.1 3バイトのmemset](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24981/4a34c318-2106-dc27-7021-d13c0594ca6a.jpeg "Fig.1 3バイトのmemset")

Fig.1 3バイトのmemset

<br />

この場合、strh valw,[dstend,-2] 命令では16bit(2byte)にアライメントされていないアドレスに16bit(2byte)を書き込むことになり、非キャッシュ領域だとバスエラーになります。

memset(0x1000, 0xCC, 20) のように20バイト分セットする場合、下の図のように命令が実行されます。


![Fig.2 20バイトのmemset](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24981/0fb97ade-372c-24f1-9573-270e2b747414.jpeg "Fig.2 20バイトのmemset")

Fig.2 20バイトのmemset

<br />

この場合、str q0,[dstend,-16] 命令では128bit(16byte)にアライメントされていないアドレスにq0(128bit の Advanced SIMD register) の値を書き込むことになり、非キャッシュ領域だとバスエラーになります。




# 対処方法


基本的に ARM64 では非キャッシュ領域に memset() は使わない方が良いでしょう。どうしても使用せざるを得ない場合は、0クリアしない、転送開始アドレスと転送サイズがアライメント境界になるようにする、等の注意が必要です。

PL 側の回路のレジスタ空間を uio でユーザー空間に割り当てた時は、横着してmemset() を使ってはいけません。レジスタ一つ一つを設定してください。

udmabuf で DMA バッファをユーザー空間に割り当てる場合は、O_SYNC を指定して open すると非キャッシュ領域になってしまいます。その時は memset() を使わないでください。udmabuf のキャッシュ制御に関しては udmabuf の資料を参考にしてください。






# 参考


*  https://code.woboq.org/userspace/glibc/sysdeps/aarch64/memset.S.html
*  https://github.com/ikwzm/udmabuf
*  https://github.com/ikwzm/udmabuf/blob/master/Readme.ja.md
*  http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801cj/pge1427897666605_00003.html


# ソースコード


最後にmemset() のソースコードを示します。見ての通り、ARM64(aarch64) に特化しているためにアセンブリ言語で記述されています。


```C:memset.S
/* Copyright (C) 2012-2019 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.
   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.
   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library.  If not, see
   <http://www.gnu.org/licenses/>.  */
#include <sysdep.h>
#include "memset-reg.h"
#ifndef MEMSET
# define MEMSET memset
#endif

/* Assumptions:
 *
 * ARMv8-a, AArch64, unaligned accesses
 *
 */
ENTRY_ALIGN (MEMSET, 6)
        DELOUSE (0)
        DELOUSE (2)
        dup        v0.16B, valw
        add        dstend, dstin, count
        cmp        count, 96
        b.hi        L(set_long)
        cmp        count, 16
        b.hs        L(set_medium)
        mov        val, v0.D[0]
        /* Set 0..15 bytes.  */
        tbz        count, 3, 1f
        str        val, [dstin]
        str        val, [dstend, -8]
        ret
        nop
1:      tbz        count, 2, 2f
        str        valw, [dstin]
        str        valw, [dstend, -4]
        ret
2:     cbz        count, 3f
        strb        valw, [dstin]
        tbz        count, 1, 3f
        strh        valw, [dstend, -2]
3:      ret
        /* Set 17..96 bytes.  */
L(set_medium):
        str        q0, [dstin]
        tbnz        count, 6, L(set96)
        str        q0, [dstend, -16]
        tbz        count, 5, 1f
        str        q0, [dstin, 16]
        str        q0, [dstend, -32]
1:      ret

        .p2align 4
        /* Set 64..96 bytes.  Write 64 bytes from the start and
           32 bytes from the end.  */
L(set96):
        str        q0, [dstin, 16]
        stp        q0, q0, [dstin, 32]
        stp        q0, q0, [dstend, -32]
        ret
        .p2align 3
        nop
L(set_long):
        and        valw, valw, 255
        bic        dst, dstin, 15
        str        q0, [dstin]
        cmp        count, 256
        ccmp        valw, 0, 0, cs
        b.eq        L(try_zva)
L(no_zva):
        sub        count, dstend, dst        /* Count is 16 too large.  */
        sub        dst, dst, 16                /* Dst is biased by -32.  */
        sub        count, count, 64 + 16        /* Adjust count and bias for loop.  */
1:      stp        q0, q0, [dst, 32]
        stp        q0, q0, [dst, 64]!
L(tail64):
        subs        count, count, 64
        b.hi        1b
2:      stp        q0, q0, [dstend, -64]
        stp        q0, q0, [dstend, -32]
        ret

L(try_zva):
#ifdef ZVA_MACRO
        zva_macro
#else
        .p2align 3
        mrs        tmp1, dczid_el0
        tbnz        tmp1w, 4, L(no_zva)
        and        tmp1w, tmp1w, 15
        cmp        tmp1w, 4        /* ZVA size is 64 bytes.  */
        b.ne         L(zva_128)
        /* Write the first and last 64 byte aligned block using stp rather
           than using DC ZVA.  This is faster on some cores.
         */
L(zva_64):
        str        q0, [dst, 16]
        stp        q0, q0, [dst, 32]
        bic        dst, dst, 63
        stp        q0, q0, [dst, 64]
        stp        q0, q0, [dst, 96]
        sub        count, dstend, dst        /* Count is now 128 too large.        */
        sub        count, count, 128+64+64        /* Adjust count and bias for loop.  */
        add        dst, dst, 128
        nop
1:      dc         zva, dst
        add        dst, dst, 64
        subs       count, count, 64
        b.hi        1b
        stp        q0, q0, [dst, 0]
        stp        q0, q0, [dst, 32]
        stp        q0, q0, [dstend, -64]
        stp        q0, q0, [dstend, -32]
        ret

        .p2align 3
L(zva_128):
        cmp        tmp1w, 5        /* ZVA size is 128 bytes.  */
        b.ne        L(zva_other)
        str        q0, [dst, 16]
        stp        q0, q0, [dst, 32]
        stp        q0, q0, [dst, 64]
        stp        q0, q0, [dst, 96]
        bic        dst, dst, 127
        sub        count, dstend, dst        /* Count is now 128 too large.        */
        sub        count, count, 128+128        /* Adjust count and bias for loop.  */
        add        dst, dst, 128
1:      dc        zva, dst
        add        dst, dst, 128
        subs        count, count, 128
        b.hi        1b
        stp        q0, q0, [dstend, -128]
        stp        q0, q0, [dstend, -96]
        stp        q0, q0, [dstend, -64]
        stp        q0, q0, [dstend, -32]
        ret

L(zva_other):
        mov        tmp2w, 4
        lsl        zva_lenw, tmp2w, tmp1w
        add        tmp1, zva_len, 64        /* Max alignment bytes written.         */
        cmp        count, tmp1
        blo        L(no_zva)
        sub        tmp2, zva_len, 1
        add        tmp1, dst, zva_len
        add        dst, dst, 16
        subs        count, tmp1, dst        /* Actual alignment bytes to write.  */
        bic        tmp1, tmp1, tmp2        /* Aligned dc zva start address.  */
        beq        2f
1:      stp        q0, q0, [dst], 64
        stp        q0, q0, [dst, -32]
        subs        count, count, 64
        b.hi        1b
2:      mov        dst, tmp1
        sub        count, dstend, tmp1        /* Remaining bytes to write.  */
        subs        count, count, zva_len
        b.lo        4f
3:      dc        zva, dst
        add        dst, dst, zva_len
        subs        count, count, zva_len
        b.hs        3b
4:      add        count, count, zva_len
        sub        dst, dst, 32                /* Bias dst for tail loop.  */
        b        L(tail64)
#endif
END (MEMSET)
libc_hidden_builtin_def (MEMSET)
```