はじめに
ZynqMP(ARM64) で PL 部とデータのやりとりをする際、 uio やudmabuf を使って mmap() でユーザー空間にアクセス領域を確保することがあります。uio はPL部のレジスタ空間をユーザー空間にマッピングすることが多く、そのために通常は非キャッシュ領域になります。
こうしてユーザー空間にマッピングした非キャッシュ領域に対して、libc の memset() 関数を使ってメモリを初期化すると、バスエラーが起きることがあります。
この記事ではバスエラーが起きるメカニズムを説明します。
glibc の ARM64(aarch64)用の memset() のソースコードは次のURLを参考にしました。この記事の最後にもソースコードを載せていますので参考にしてください。みてのとおり、バリバリのアセンブリ言語で書かれています。
バスエラーが起きるメカニズム
非キャッシュ領域でmemset() を実行するとバスエラーになる要因は、実は2つあります。一つ目は データキャッシュクリア 命令によるメモリクリアを使っていること、二つ目は非アライメントなストア命令を使っていることです。
データキャッシュクリア命令のよるメモリクリア
例えば次の例のように、セットする値が0で、ある程度セットするバイト数が多い場合、ARM64 用のmemset() は データキャッシュクリア命令(dc zva)を使ってメモリクリアを行うことがあります。
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 から負方向にオフセットされたアドレスにデータをストアする命令が見受けられます。
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)を足した値、つまり最後のアドレス+1を示しています。先頭アドレスと転送サイズの組み合わせにより、dstend は非アライメントなアドレスを指すことがあります。
例えば、memset(0x1000, 0xCC, 3) のように3バイト分セットする場合、下の図のように命令が実行されます。
Fig.1 3バイトのmemset
この場合、strh valw,[dstend,-2] 命令では16bit(2byte)にアライメントされていないアドレスに16bit(2byte)を書き込むことになり、非キャッシュ領域だとバスエラーになります。
memset(0x1000, 0xCC, 20) のように20バイト分セットする場合、下の図のように命令が実行されます。
Fig.2 20バイトのmemset
この場合、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) に特化しているためにアセンブリ言語で記述されています。
/* 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)