자바스크립트를 활성화 해주세요

System call 확인하기 (strace)

 ·  ☕ 9 min read

시스템 콜은 운영체제의 커널이 제공하는 서비스를 응용프로그램이 사용하기 위한 인터페이스라고 운영체제 강의에서 배웠을 것이다. 보통 커널이 제공하는 기능은 자원 관리나 IO와 관계되어있기 때문에, 일반적인 응용프로그램은 자기도 모르게 숨쉬듯이 자주 시스템 콜을 호출한다고 한다. 그렇다면 어떤 프로그램이 어떤 시스템 콜을 호출하는지 확인할 수 없을까? 이걸 추적할 수 있는 도구인 strace를 알아보자.

strace 도구란?

먼저 man에서 설명하는 strace를 소개하는 한 문장부터 보자.

strace - trace system calls and signals

말 그대로 시스템 콜과 시그널이 발생한 것을 추적하는 도구다. 부가적인 설명 전에 먼저 실행해보자.

$ strace cat log.txt > /dev/null
execve("/usr/bin/cat", ["cat", "log.txt"], 0x7ffc736a98c8 /* 61 vars */) = 0
brk(NULL)                               = 0x562a9b535000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffcc2b4b820) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8f08b48000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=89945, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 89945, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8f08b32000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\211\303\313\205\371\345PFwdq\376\320^\304A"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=2216304, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8f0890a000
mmap(0x7f8f08932000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7f8f08932000
mmap(0x7f8f08ac7000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7f8f08ac7000
mmap(0x7f8f08b1f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7f8f08b1f000
mmap(0x7f8f08b25000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f8f08b25000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8f08907000
arch_prctl(ARCH_SET_FS, 0x7f8f08907740) = 0
set_tid_address(0x7f8f08907a10)         = 567554
set_robust_list(0x7f8f08907a20, 24)     = 0
rseq(0x7f8f089080e0, 0x20, 0, 0x53053053) = 0
mprotect(0x7f8f08b1f000, 16384, PROT_READ) = 0
mprotect(0x562a9b4cf000, 4096, PROT_READ) = 0
mprotect(0x7f8f08b82000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7f8f08b32000, 89945)           = 0
getrandom("\xc9\xee\xae\x4f\x5f\x0d\xbd\x2f", 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0x562a9b535000
brk(0x562a9b556000)                     = 0x562a9b556000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=7057008, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 7057008, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8f0824c000
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x3), ...}, AT_EMPTY_PATH) = 0
openat(AT_FDCWD, "log.txt", O_RDONLY) = 3
newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=2184, ...}, AT_EMPTY_PATH) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8f0822a000
read(3, "Test case                 pg    "..., 131072) = 2184
write(1, "Test case                 pg    "..., 2184) = 2184
read(3, "", 131072)                     = 0
munmap(0x7f8f0822a000, 139264)          = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

cat 명령으로 log.txt 파일의 내용을 읽을 때, 어떤 시스템 콜이 호출되는지 확인할 수 있다. (여기서 cat의 출력은 fd 1(stdout)으로 출력되고, strace의 결과는 fd 2(stderr)으로 출력되므로, 출력 내용을 생략하기 위해 cat의 결과만 /dev/null로 버렸다.)

이제 본격적으로 C언어의 주요 함수/시스템 콜 API를 호출했을 때 실제 시스템 콜은 어떻게 발생하는지 확인해보자.

빌드 차이, C언어 구현체 차이 확인하기

모든 프로그래밍의 시작 예제인 helloworld 프로그램을 추적해보자.

#include <stdio.h>

int main(void)
{
        printf("hello");
}
$ gcc hello.c -o hello
$ strace ./hello > /dev/null
execve("./hello", ["./hello"], 0x7fffa6aca0c0 /* 61 vars */) = 0
brk(NULL)                               = 0x5634337f0000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd62ed1ed0) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb506e2d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=89945, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 89945, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb506e17000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\211\303\313\205\371\345PFwdq\376\320^\304A"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=2216304, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb506bef000
mmap(0x7fb506c17000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7fb506c17000
mmap(0x7fb506dac000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7fb506dac000
mmap(0x7fb506e04000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7fb506e04000
mmap(0x7fb506e0a000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb506e0a000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb506bec000
arch_prctl(ARCH_SET_FS, 0x7fb506bec740) = 0
set_tid_address(0x7fb506beca10)         = 574935
set_robust_list(0x7fb506beca20, 24)     = 0
rseq(0x7fb506bed0e0, 0x20, 0, 0x53053053) = 0
mprotect(0x7fb506e04000, 16384, PROT_READ) = 0
mprotect(0x563433516000, 4096, PROT_READ) = 0
mprotect(0x7fb506e67000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7fb506e17000, 89945)           = 0
newfstatat(1, "", {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x3), ...}, AT_EMPTY_PATH) = 0
ioctl(1, TCGETS, 0x7ffd62ed1740)        = -1 ENOTTY (Inappropriate ioctl for device)
getrandom("\x51\x16\xe0\x1f\x58\x7f\xe1\x79", 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0x5634337f0000
brk(0x563433811000)                     = 0x563433811000
write(1, "hello", 5)                    = 5
exit_group(0)                           = ?
+++ exited with 0 +++

분명 예제는 간단한 helloworld 프로그램이다. 하지만 생각 이상으로 많은 시스템 콜이 호출된 것을 볼 수 있다.
여기서 핵심이 되는 부분은 제일 아래쪽에 있는 write(1, "hello", 5) 부분이다. write() 시스템 콜은 파일 디스크립터에게 버퍼의 내용을 지정된 크기만큼 쓰게 하는데, 위 추적된 내용을 해석해보면 fd 1(stdout)에 "hello"라는 버퍼의 5바이트를 쓰라고 명령한 것이다. 그리고 write()가 쓰는 데 성공한 길이인 5를 반환했다.

그런데 우리가 기대하는 시스템 콜에 비해 너무 많은 시스템 콜이 추적되고 있다. 제일 초반에 보면 execve() 시스템 콜부터 호출되는걸 볼 수 있다. 즉, strace가 추적을 시작하는 순간은 명령어(프로그램)를 실행하기 위해 프로세스의 이미지가 교체되는 순간부터 추적이 시작된 것이라고 볼 수 있다. 그 이후 내용을 보면 openat() 시스템 콜/etc/ld.so.cache나, /lib/x86_64-linux-gnu/libc.so.6 파일을 열고 읽는 것을 확인할 수 있다. ld.so라던가, libc.so.6를 보면 C언어 함수의 구현을 동적 링크 라이브러리로 사용하는 것을 알 수 있다. 그렇다면 프로그램을 정적 빌드하거나, 빌드에 사용되는 C언어의 구현체를 더 가벼운 musl로 바꿔 실험해보자. (기존 gcc 빌드는 아마 glibc 구현체를 사용했을 것이다.)

$ gcc hello.c -o hello
$ strace ./hello > /dev/null
execve("./hello", ["./hello"], 0x7fffa6aca0c0 /* 61 vars */) = 0
brk(NULL)                               = 0x5634337f0000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd62ed1ed0) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb506e2d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=89945, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 89945, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb506e17000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\211\303\313\205\371\345PFwdq\376\320^\304A"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=2216304, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb506bef000
mmap(0x7fb506c17000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7fb506c17000
mmap(0x7fb506dac000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7fb506dac000
mmap(0x7fb506e04000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7fb506e04000
mmap(0x7fb506e0a000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb506e0a000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb506bec000
arch_prctl(ARCH_SET_FS, 0x7fb506bec740) = 0
set_tid_address(0x7fb506beca10)         = 574935
set_robust_list(0x7fb506beca20, 24)     = 0
rseq(0x7fb506bed0e0, 0x20, 0, 0x53053053) = 0
mprotect(0x7fb506e04000, 16384, PROT_READ) = 0
mprotect(0x563433516000, 4096, PROT_READ) = 0
mprotect(0x7fb506e67000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7fb506e17000, 89945)           = 0
newfstatat(1, "", {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x3), ...}, AT_EMPTY_PATH) = 0
ioctl(1, TCGETS, 0x7ffd62ed1740)        = -1 ENOTTY (Inappropriate ioctl for device)
getrandom("\x51\x16\xe0\x1f\x58\x7f\xe1\x79", 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0x5634337f0000
brk(0x563433811000)                     = 0x563433811000
write(1, "hello", 5)                    = 5
exit_group(0)                           = ?
+++ exited with 0 +++
$ gcc hello.c --static -o hello_static
$ strace ./hello_static > /dev/null
execve("./hello_static", ["./hello_static"], 0x7fff95ca98e0 /* 61 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffedcdea610) = -1 EINVAL (Invalid argument)
brk(NULL)                               = 0x1081000
brk(0x1081dc0)                          = 0x1081dc0
arch_prctl(ARCH_SET_FS, 0x10813c0)      = 0
set_tid_address(0x1081690)              = 575041
set_robust_list(0x10816a0, 24)          = 0
rseq(0x1081d60, 0x20, 0, 0x53053053)    = 0
uname({sysname="Linux", nodename="JSYoo5B-Base", ...}) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", "/home/jsyoo5b/Workspace/OSC_10th"..., 4096) = 66
getrandom("\x00\xde\x30\xb0\x50\x53\xf5\x5c", 8, GRND_NONBLOCK) = 8
brk(0x10a2dc0)                          = 0x10a2dc0
brk(0x10a3000)                          = 0x10a3000
mprotect(0x4c1000, 16384, PROT_READ)    = 0
newfstatat(1, "", {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x3), ...}, AT_EMPTY_PATH) = 0
ioctl(1, TCGETS, 0x7ffedcde9c60)        = -1 ENOTTY (Inappropriate ioctl for device)
write(1, "hello", 5)                    = 5
exit_group(0)                           = ?
+++ exited with 0 +++
$ musl-gcc hello.c -o hello_musl
$ strace ./hello_musl > /dev/null
execve("./hello_musl", ["./hello_musl"], 0x7fff25b7ea00 /* 61 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x7fc996a02a08) = 0
set_tid_address(0x7fc996a02e50)         = 621099
brk(NULL)                               = 0x5620b3fc1000
brk(0x5620b3fc3000)                     = 0x5620b3fc3000
mmap(0x5620b3fc1000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5620b3fc1000
mprotect(0x7fc9969ff000, 4096, PROT_READ) = 0
mprotect(0x5620b2c90000, 4096, PROT_READ) = 0
ioctl(1, TIOCGWINSZ, 0x7ffc922454a0)    = -1 ENOTTY (Inappropriate ioctl for device)
writev(1, [{iov_base="hello", iov_len=5}, {iov_base=NULL, iov_len=0}], 2) = 5
exit_group(0)                           = ?
+++ exited with 0 +++
$ musl-gcc hello.c --static -o hello_musl_static
$ strace ./hello_musl_static > /dev/null
execve("./hello_musl_static", ["./hello_musl_static"], 0x7fff617bf210 /* 61 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x407678)       = 0
set_tid_address(0x4077b0)               = 621372
ioctl(1, TIOCGWINSZ, 0x7ffefed10f30)    = -1 ENOTTY (Inappropriate ioctl for device)
writev(1, [{iov_base="hello", iov_len=5}, {iov_base=NULL, iov_len=0}], 2) = 5
exit_group(0)                           = ?
+++ exited with 0 +++

전체적으로 정적 빌드 시 동적 링크 라이브러리를 불러오는 과정이 사라지기 때문에 호출되는 시스템 콜의 개수가 줄어드는 것을 볼 수 있다. 또한 musl이 더 간단한 구현체인 만큼 시스템 콜 횟수가 급격히 줄어든 것도 확인할 수 있다.

함수 분석에 불필요한 시스템 콜을 줄이기 위해 앞으로는 musl에 정적 빌드만 사용하도록 하자. 앞으로 분석하는 데 있어 어쩔 수 없이 호출되는 시스템 콜을 분석하기 위해 아래와 같이 아무 동작도 하지 않는 프로그램의 시스템 콜을 확인해보자.

int main()
{
}
$ musl-gcc blank.c --static -o blank
$ strace ./blank
execve("./blank", ["./blank"], 0x7ffde7570e10 /* 61 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x404158)       = 0
set_tid_address(0x404290)               = 642329
exit_group(0)                           = ?
+++ exited with 0 +++

execve()는 이전에 언급했듯 프로세스의 이미지를 교체하기 위해 사용되었다.
arch_prctl()set_tid_address()는 새로운 프로세스를 위해 준비하는 시스템 콜로 보이는데, 프로세스 생성 및 교체 과정에 대한 이해가 아직 부족해서 설명만으로 각 시스템 콜이 왜 필요한지에 대해 정확한 이해는 하지 못했다.
exit_group()은 프로세스를 종료시키면서 반환 코드를 설정하는 시스템 콜이다.

즉, 앞으로 내가 작성한 프로그램을 분석하는 데 있어 여기 있는 초반, 후반의 시스템 콜을 무시하면 내가 작성한 함수가 호출하는 시스템 콜만 확인할 수 있다.

hello에서 핵심 시스템 콜 분석하기

$ musl-gcc hello.c --static -o hello
$ strace ./hello > /dev/null
execve("./hello", ["./hello"], 0x7ffeca113070 /* 61 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x407678)       = 0
set_tid_address(0x4077b0)               = 659153
ioctl(1, TIOCGWINSZ, {ws_row=56, ws_col=192, ws_xpixel=3072, ws_ypixel=1792}) = 0
writev(1, [{iov_base="hello", iov_len=5}, {iov_base=NULL, iov_len=0}], 2hello) = 5
exit_group(0)                           = ?
+++ exited with 0 +++
ioctl(1, TIOCGWINSZ, {ws_row=56, ws_col=192, ws_xpixel=3072, ws_ypixel=1792}) = 0
writev(1, [{iov_base="hello", iov_len=5}, {iov_base=NULL, iov_len=0}], 2) = 5

이전 glibc의 결과에서는 write()를 사용했지만, musl의 경우 writev()를 사용하고 있다. 그리고 실제 출력 이전에 ioctl()을 호출하는 것을 확인할 수 있다.

ioctl()은 실제 파일에 입출력 하지 않고, 해당 파일의 속성 등을 변경할 때 사용하는 시스템 콜이다. 여기서는 fd 1(stdout)의 설정을 변경하는데, musl과 glibc에서 각각 설정하는 내용이 다르다. 현재 기준이 되는 musl에서 호출한 목적은 tty_ioctl() 기능을 사용하려 한 것 같다. 들어간 구조체의 내용을 확인해 보면 현재 콘솔의 크기(가로 192, 세로 56, x축 픽셀 3072, y축 픽셀 1792)를 설정한 것으로 보인다.

write()writev()의 차이는 한 개 버퍼에 대한 쓰기냐, 여러 버퍼에 대한 쓰기 여부가 다르다. 아마 musl에서 write() 할 버퍼의 크기가 커질 경우 여러 번 write()를 호출하는 대신 한 번의 writev()로 시스템 콜 요청/반환 과정의 부하를 줄이려 한 것으로 예상된다.

malloc 시스템 콜 확인하기

메모리를 동적으로 할당받는 malloc()은 어떤 시스템 콜로 구현되어 있는지 확인해보자.

참고로 malloc()의 요청 메모리 크기가 작을 경우, 구현체에 따라 시스템 콜 없이 구현체의 남는 공간이나 이미 할당받은 공간 중 남는 부분을 넘겨주도록 구현하기도 한다. 이렇게 될 경우 기대하는 시스템 콜이 일어나는 과정을 확인할 수 없으므로 예제 코드에서는 큰 공간의 메모리를 여러 번 요청하도록 작성했다.

#include <stdlib.h>

int main(void)
{
	for (int i = 0; i < 4; i++) {
		char* addr = (char*)malloc(1024 * 1024);
	}

	return 0;
}
$ musl-gcc malloc.c --static -o malloc
$ strace ./malloc > /dev/null
execve("./malloc", ["./malloc"], 0x7ffd49b76cc0 /* 61 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x404178)       = 0
set_tid_address(0x4042b0)               = 642325
brk(NULL)                               = 0x585000
brk(0x685000)                           = 0x685000
brk(0x785000)                           = 0x785000
brk(0x885000)                           = 0x885000
brk(0x985000)                           = 0x985000
exit_group(0)                           = ?
+++ exited with 0 +++
brk(NULL)                               = 0x585000
brk(0x685000)                           = 0x685000
brk(0x785000)                           = 0x785000
brk(0x885000)                           = 0x885000
brk(0x985000)                           = 0x985000

brk()를 사용해서 malloc()을 구현하는 원리는 아래와 같이 알고 있다.

  1. 제일 처음 brk(NULL)로 현재 Data Segment의 최댓값(마지막 주소)을 알아낸다.
  2. Data Segment는 전역 변수, 함수의 static 변수들을 보관하기 위한 공간이다.
  3. brk()로 Data Segment의 크기를 늘리면 그만큼 사용할 수 있는 주소 공간이 늘어나게 된다.
  4. malloc()의 경우, 늘어난 주소공간에서 추가적인 메모리 관리도 진행한다.
    (malloc()에서 할당하는 주소 앞의 공간을 할당하여 현재 할당한 메모리 크기를 메모해 놓기도 한다.)
  5. free()로 메모리를 해제하는 경우, 보통은 추후 malloc() 사용을 예상하여 brk()로 Data Segment의 크기를 조정하지 않는 편이다.
    하지만 만약 brk()로 기존 Data Segment의 크기보다 작게 설정하면 주소 공간을 반납하게 된다.

위 예시에서 핵심 시스템 콜을 확인해보면 brk()가 다섯 번 호출된 것을 확인할 수 있다. 첫 brk(NULL)은 현재 Data Segment의 최대 크기를 알기 위함이고, 나머지 4번의 brk() 호출은 최근 Data Segment의 최댓값을 기준으로 0x100000(1024 * 1024)씩 더해서 요청하는 것을 확인할 수 있다.

일반적인 접근법은 여기서 확인한 것처럼 brk()를 활용하는 것이다. 하지만 glibc의 경우, mmap()을 통해 메모리를 할당받는다. 직접 확인해보자.

$ gcc malloc.c --static -o malloc
$ strace ./malloc > /dev/null
execve("./malloc_gcc", ["./malloc_gcc"], 0x7ffc8cd7f9f0 /* 61 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffca5f14b20) = -1 EINVAL (Invalid argument)
brk(NULL)                               = 0x5a8000
brk(0x5a8dc0)                           = 0x5a8dc0
arch_prctl(ARCH_SET_FS, 0x5a83c0)       = 0
set_tid_address(0x5a8690)               = 676168
set_robust_list(0x5a86a0, 24)           = 0
rseq(0x5a8d60, 0x20, 0, 0x53053053)     = 0
uname({sysname="Linux", nodename="JSYoo5B-Base", ...}) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
readlink("/proc/self/exe", "/home/jsyoo5b/Workspace/OSC_10th"..., 4096) = 64
getrandom("\x31\x9b\x9e\xd5\xb8\x3e\x21\xe2", 8, GRND_NONBLOCK) = 8
brk(0x5c9dc0)                           = 0x5c9dc0
brk(0x5ca000)                           = 0x5ca000
mprotect(0x4c1000, 16384, PROT_READ)    = 0
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802247f000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802237e000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802227d000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802217c000
exit_group(0)                           = ?
+++ exited with 0 +++
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802247f000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802237e000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802227d000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f802217c000

mmap()은 보통 파일이나 장치를 메모리에 매핑시켜 사용하는 용도로 알고 있을 것이다. mmap()의 동작 방식은 커널로부터 가상 주소 공간을 할당받는다. 이때 추가 제공되는 prot, flags, fd에 따라 파일 혹은 장치에 매핑할 수도 있는 것이다. 만약 파일 디스크립터를 설정하지 않고, flag도 적절하게 설정한다면 가상 주소 공간만을 할당받게 된다.

참고로 여기서 mmap()의 크기로 결정된 1052672는 요청한 1048576(1024 * 1024)보다 크다. 앞에서 설명했듯 malloc()에서는 현재 동적 할당되어 사용 중인 메모리의 크기에 대한 정보 등을 보관해야 하므로 조금 더 크게 할당을 요청한 것으로 보인다.

다른 프로그램의 시스템 콜 추적하기

위에서 했던 것과 같이, 다른 기능의 코드를 작성하고 실제 시스템 콜의 발생 방식을 추적해 볼 수 있다.

여기에 이 글에서 다룬 예제뿐만 아니라, 다른 시스템 콜을 C언어 API로 호출해보는 프로그램들, 빌드 및 실행 자동화 스크립트 등을 확인할 수 있다.


JaeSang Yoo
글쓴이
JaeSang Yoo
The Programmer

목차