こんにちは、 @kz_morita です。
みかん本 で自作OSの開発を進めていくなかで学びになったメモリ周りの知識についてまとめます.
プロセスのセグメント配置について
実行されたプログラムは,メモリ上にロードされ実行されますがざっくりと以下の様な構造になっています.
- .text : プログラムのうち機械語を格納するセグメント
- .data : プログラム上の初期化されたデータを格納するセグメント
- .bss : 初期化されていないデータを格納するセグメント.OS (exec) が自動で0に初期化する
スタックは,関数呼び出しなどで使用され,保存される情報とともに局所変数(auto 変数) や関数の引数が置かれます.大きいアドレスから小さいアドレスの方向に向かって伸びていきます.
ヒープは,動的にメモリを割り当てる場所で,主に malloc などによりメモリ確保されるとヒープに配置されます.小さいアドレスから大きいアドレスの方向に向かって伸びていきます.
実際のセグメントを見る
実際に cpp のコードをビルドした成果物の中身を以下のコマンドで確認することができます.
$ objdump -h ./main
以下のようなセクションの情報が見れます.(.text, .data, .bss などが確認できます)
./main: ファイル形式 elf64-x86-64
セクション:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000238 0000000000000238 00000238 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 0000000000000254 0000000000000254 00000254 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000274 0000000000000274 00000274 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 0000001c 0000000000000298 0000000000000298 00000298 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 000000d8 00000000000002b8 00000000000002b8 000002b8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 000000a4 0000000000000390 0000000000000390 00000390 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 00000012 0000000000000434 0000000000000434 00000434 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000030 0000000000000448 0000000000000448 00000448 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rela.dyn 000000c0 0000000000000478 0000000000000478 00000478 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.plt 00000048 0000000000000538 0000000000000538 00000538 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000017 0000000000000580 0000000000000580 00000580 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000040 00000000000005a0 00000000000005a0 000005a0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 00000000000005e0 00000000000005e0 000005e0 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 000002a2 00000000000005f0 00000000000005f0 000005f0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000009 0000000000000894 0000000000000894 00000894 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 0000005b 00000000000008a0 00000000000008a0 000008a0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 0000004c 00000000000008fc 00000000000008fc 000008fc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 00000148 0000000000000948 0000000000000948 00000948 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000008 0000000000200da8 0000000000200da8 00000da8 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000008 0000000000200db0 0000000000200db0 00000db0 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 000001f0 0000000000200db8 0000000000200db8 00000db8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000058 0000000000200fa8 0000000000200fa8 00000fa8 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .data 00000010 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000008 0000000000201010 0000000000201010 00001010 2**0
ALLOC
24 .comment 00000029 0000000000000000 0000000000000000 00001010 2**0
CONTENTS, READONLY
また,size コマンドでバイト数を確認することも出来ます.
$ size ./main
text data bss dec hex filename
2107 616 8 2731 aab ./main
ヒープの挙動を確認する
実際のコードをもとにメモリを確保する際の挙動を確認してみます.
環境は以下のような感じです.
$ uname -a
Linux kazuki 5.4.0-73-generic #82~18.04.1-Ubuntu SMP Fri Apr 16 15:10:02 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.5 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
以下のようなコードで実験しました.
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <cstdint>
int main() {
// sbrk(0) で現在のプログラムブレークの値を取得
printf("program break\t0x%lx\n", (unsigned long)sbrk(0));
printf("program break\t0x%lx\n", (unsigned long)sbrk(0));
// malloc
printf("===== malloc ===== \n");
int *p = (int*)malloc(sizeof(int));
printf("malloc address\t0x%lx\n", (unsigned long)p);
printf("program break\t0x%lx\n", (unsigned long)sbrk(0));
unsigned long prev_brk = (unsigned long) sbrk(0);
printf("===== bulk malloc ===== \n");
for (int i = 0; i < 50000; ++i) {
long long *q = (long long*)malloc(sizeof(long long));
unsigned long current_brk = (unsigned long) sbrk(0);
if (prev_brk != current_brk) {
printf("===== move program break ===== \n");
prev_brk = current_brk;
printf("%dth malloc address\t0x%lx\n", i, (unsigned long)q);
printf("program break\t0x%lx\n", (unsigned long)sbrk(0));
}
}
printf("========================= \n");
printf("program break\t0x%lx\n", (unsigned long)sbrk(0));
return 0;
}
簡単に解説すると,long long 型のメモリ領域を malloc して,現在のプログラムブレーク(ヒープセグメントの開始位置)を表示して実際のヒープが拡張しているかをしらべています. 初期のヒープ領域がある程度余裕を持って確保されているのか,数回 malloc したくらいでは,プログラムブレークの値が変わらなかったので,50000 回 malloc して,プログラムブレークが変わった,つまりヒープ領域が新たに確保されたときに情報を表示するようにしてみました.
結果はこんな感じでした.
g++ -W main.cpp -o main && ./main
program break 0x557528772000
program break 0x557528793000
===== malloc =====
malloc address 0x557528773270
program break 0x557528793000
===== bulk malloc =====
===== move program break =====
4075th malloc address 0x557528792ff0
program break 0x5575287b4000
===== move program break =====
8299th malloc address 0x5575287b3ff0
program break 0x5575287d5000
===== move program break =====
12523th malloc address 0x5575287d4ff0
program break 0x5575287f6000
===== move program break =====
16747th malloc address 0x5575287f5ff0
program break 0x557528817000
===== move program break =====
20971th malloc address 0x557528816ff0
program break 0x557528838000
===== move program break =====
25195th malloc address 0x557528837ff0
program break 0x557528859000
===== move program break =====
29419th malloc address 0x557528858ff0
program break 0x55752887a000
===== move program break =====
33643th malloc address 0x557528879ff0
program break 0x55752889b000
===== move program break =====
37867th malloc address 0x55752889aff0
program break 0x5575288bc000
===== move program break =====
42091th malloc address 0x5575288bbff0
program break 0x5575288dd000
===== move program break =====
46315th malloc address 0x5575288dcff0
program break 0x5575288fe000
=========================
program break 0x5575288fe000
おおよそ 4224 ループで,ヒープが拡張されているので long long が 8 byte なので 4224 * 8 = 33824 byte
くらいずつヒープが拡張されていることがわかりました.
sbrk() について
sbrk は,ブログラムブレークを増減する(=ヒープを確保する)システムコールです.
正の値を引数に指定すればヒープを増やして増やす前のアドレスを返し(メモリ確保),負の値を指定すればヒープを減らして以前のアドレスを返す(メモリ解放)ものになります.
今回は 0 をわたして,プログラムブレークはそのままで現在の値を取得するといった使い方をしました.
malloc の中で,sbrk() が呼ばれています.
まとめ
プログラムが実行される際のメモリ配置と,動的にメモリ領域を確保する malloc と その中で呼ばれるシステムコールの sbrk(2) について簡単に紹介しました.
また,実際に malloc をするコードを書いて,プログラムブレークの値の変化を観察しました.
自作 OS を作っている中でどのようにメモリを確保するのかといった話題から興味がわいて調べてみましたが,プログラムがどのように動いているのかがなんとなく見えてきて非常に面白かったです.
実際に手を動かすと学びも深くなるので,自作OSという題材はすばらしいなと改めて思いました.