RISC-Vにおける.sbss .sdataとgpレジスタについて

自作OS RISC-V

リンカスクリプト、よくわからないことが非常に多いのですが、その中でも特によくわからないのが、.sbss.sdataです。なんのために存在し、.bss.dataセクションと何が違うのか、についてメモしておきます。gpレジスタはその中で面白い働きをしています。

.bss/.dataセクションとは

リンカスクリプトの役割については、以下の資料を見てもらうのが良いと思いますが、.bss.dataセクションはそれぞれ、変数の格納領域として使用され、

をそれぞれ格納します。

https://speakerdeck.com/tnishinaga/security-camp-2018-baremetal-seminar-material?slide=113

規約上、.bssセクションは、ゼロクリアされていることを、保証しなければなりません。

.bssセクションの初期化

したがって、ベアメタルプログラミングを行う際には、(それがOSか否かは問わず).bssセクションをゼロクリアします。 これが、前回の記事で次回触れると言っていたclear_bssの実装です。

void clear_bss() {
    extern unsigned int __bss_start, __bss_end;
    unsigned int* start = &__bss_start;
    unsigned int* end = &__bss_end;
    while (start < end) {
        *start = 0;
        start ++;
    }
}

このうち、__bss_start__bss_endはリンカスクリプトで定義されているシンボルです。これらは、それぞれ、.bssセクションの先頭、終端の32bitを指しています。 externキーワードを使用することで、このファイル内に定義がなくとも、リンカスクリプトで定義されているシンボルを使用することができます。

    .bss : {
      __bss_start = .;
      *(.bss .bss.*);
      . = ALIGN(16);
      *(.sbss .sbss.*);
      __bss_end = .;
      . = ALIGN(4096);
      . = . + 16384;
      stack_top = .;
    }

http://yutyan.dev/os-task-create

ちなみに、.bssセクションをクリアしている保証がない前提でコンパイルするオプションもあり、それが-fno-zero-initialized-in-bssです。

.sbss, .sdataセクションについて

sは「small」を意味します。つまり、これらのセクションは小さなデータ項目を格納するために使われるようです。(どの程度の小さいかは、おそらくアーキテクチャ依存)

.sdataセクションは小さなデータ項目を格納するために使用されます。アーキテクチャによっては、小さなデータ項目の読み書きを最適化するための特別な指令がある場合があります。それらの指令を利用して、プログラムのパフォーマンスを改善することが可能になります。.sbssセクションは小さな初期化されていないデータ項目を格納するために使用されます。

RISC-Vにおいては、gp(global pointer)レジスタを使用して、これらの小さいデータへのアクセスを最適化するようです。

最適化の例

SiFiveのブログから、最適化の例を引用します。 https://www.sifive.com/blog/all-aboard-part-3-linker-relaxation-in-riscv-toolchain 以下のようなコードを考えます。このコードは、グローバル変数へのアクセスを3回も行っています。

/* Global Variables: */
Boolean         Bool_Glob;
char            Ch_1_Glob,
                Ch_2_Glob;

Proc_4 () {
  Boolean Bool_Loc;

  Bool_Loc = Ch_1_Glob == 'A';
  Bool_Glob = Bool_Loc | Bool_Glob;
  Ch_2_Glob = 'B';
} /* Proc_4 */

このコードをアセンブルすると、以下のようなアセンブリコードになります。

0000000040400826 <Proc_4>:
    40400826:   3fc00797                auipc   a5,0x3fc00
    4040082a:   f777c783                lbu     a5,-137(a5) # 8000079d <Ch_1_Glob>
    4040082e:   3fc00717                auipc   a4,0x3fc00
    40400832:   f7272703                lw      a4,-142(a4) # 800007a0 <Bool_Glob>
    40400836:   fbf78793                addi    a5,a5,-65
    4040083a:   0017b793                seqz    a5,a5
    4040083e:   8fd9                    or      a5,a5,a4
    40400840:   3fc00717                auipc   a4,0x3fc00
    40400844:   f6f72023                sw      a5,-160(a4) # 800007a0 <Bool_Glob>
    40400848:   3fc00797                auipc   a5,0x3fc00
    4040084c:   04200713                li      a4,66
    40400850:   f4e78a23                sb      a4,-172(a5) # 8000079c <Ch_2_Glob>
    40400854:   8082                    ret

見るとわかるように、auipc命令を使用して、グローバル変数のアドレスを計算しています。4回アドレスを計算していますが、それぞれで計算されているアドレスは12bitのオフセット内に収まっています。そこで、以下のようなリンカスクリプトを用意します。

/* We want the small data sections together, so single-instruction offsets
   can access them all, and initialized data all before uninitialized, so
   we can shorten the on-disk segment size.  */
.sdata          :
{
  __global_pointer$ = . + 0x800;
  *(.srodata.cst16) *(.srodata.cst8) *(.srodata.cst4) *(.srodata.cst2) *(.srodata .srodata.*)
  *(.sdata .sdata.* .gnu.linkonce.s.*)
}
_edata = .; PROVIDE (edata = .);
. = .;
__bss_start = .;
.sbss           :
{
  *(.dynsbss)
  *(.sbss .sbss.* .gnu.linkonce.sb.*)
  *(.scommon)

このリンカスクリプトでは、.sdataと.sbssを隣り合うようにして、オフセットを利用したメモリアクセス命令でアクセスできるようにしています。 __global_pointer$が、.sdataの先頭から0x800先を指しています。これによって12bitのオフセットで、.sdataの先頭にアクセスすることが出来ます。リンカは、このシンボルが定義されていると、この値がgpレジスタに入っていると仮定し、その±12bitオフセット以内のアクセスを最適化に使用することが出来るようになります。以下は、初期化時にgpを設定するアセンブリコードです。

.option push
.option norelax
1:auipc gp, %pcrel_hi(__global_pointer$)
  addi  gp, gp, %pcrel_lo(1b)
.option pop

ここでは、norelaxを設定しています。そうしないと、リンカーはmv gp, gpといった最適化をしてしまいます。 このgpを利用して最適化すると、アセンブリが以下のようになります。

00000000400003f0 <Proc_4>:
    400003f0:   8651c783                lbu     a5,-1947(gp) # 80001fbd <Ch_1_Glob>
    400003f4:   8681a703                lw      a4,-1944(gp) # 80001fc0 <Bool_Glob>
    400003f8:   fbf78793                addi    a5,a5,-65
    400003fc:   0017b793                seqz    a5,a5
    40000400:   00e7e7b3                or      a5,a5,a4
    40000404:   86f1a423                sw      a5,-1944(gp) # 80001fc0 <Bool_Glob>
    40000408:   04200713                li      a4,66
    4000040c:   86e18223                sb      a4,-1948(gp) # 80001fbc <Ch_2_Glob>
    40000410:   00008067                ret

確かに、メモリへのアクセスの前にアドレスをむやみやたらと計算せずに、オフセットを使用して美しいやり方でアクセスしています。こういうやり方で、.sdata及び、.sbssセクションには、なるべく少ない命令でアクセスできるように、小さいデータを詰め込むわけです。ちなみにと言ってはなんですが、現時点での自作OSは、-mno-relaxにより、これらの最適化は一旦行わないようにしています。

まとめ

.sbss及び.sdataセクションの意味とそのgpとの関係について、最適化が関わっていることに触れながらまとめました。最適化する際はこういうことを考えているんだなということがわかり、面白かったです。


© 2023 Yuto Nakamura   •  Theme  Moonwalk