- Today
- Total
Byeo
추가한 System Call을 함수 명으로 호출하기 (glibc) 본문
지난 포스트에서 추가한 리눅스 시스템콜을 사용하기 위해서는 위에서 기술된 2번 항목처럼 syscall에 syscall_no를 넣어주면서 호출을 했어야 했는데요.
C언어에서 byeo_hello(string, strlen)을 직접 호출하고 싶으면 어떻게 해야 할까요? 그 방법은 사실 커널보다는 glibc의 수정에 있었습니다.
이번 포스트에서는 시스템콜 함수명을 직접 이용해 호출할 수 있도록 설정을 해볼 예정입니다.
0. syscall( ) 흐름
시작 전에, syscall() 함수는 어떻게 실행될지 잠깐 살펴보려고 합니다. 이 함수는 glibc의 sysdeps/unix/sysv/linux/x86_64/syscall.S에 있는 함수입니다. 해당 함수는 syscall number를 별도의 register에 빼낸 뒤에 뒤의 인자들을 하나씩 당겨주는 것으로 보입니다.
#include <sysdep.h>
/* Please consult the file sysdeps/unix/sysv/linux/x86-64/sysdep.h for
more information about the value -4095 used below. */
/* Usage: long syscall (syscall_number, arg1, arg2, arg3, arg4, arg5, arg6)
We need to do some arg shifting, the syscall_number will be in
rax. */
.text
ENTRY (syscall)
movq %rdi, %rax /* Syscall number -> rax. */
movq %rsi, %rdi /* shift arg1 - arg5. */
movq %rdx, %rsi
movq %rcx, %rdx
movq %r8, %r10
movq %r9, %r8
movq 8(%rsp),%r9 /* arg6 is on the stack. */
syscall /* Do the system call. */
cmpq $-4095, %rax /* Check %rax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
ret /* Return to caller. */
PSEUDO_END (syscall)
아래 표에 맞춰서 말이죠. 기존에 시스템 콜 번호 (byeo_hello는 449)는 %rax로 넣어주는 모습입니다. 그다음, x86_64 기준으로 첫 번째 인자(arg1)는 %rdi에 들어가도록, 그리고 그 뒤 인자들도 순차적으로 당겨주는 것으로 보입니다. (여기서 알 수 있는 사실은 x86에서 system call의 인자는 최대 6개까지 사용할 수 있네요.)
그리고 어셈블리 중간에 보이는 'syscall /* Do the system call. */' 은 삽질 결과 인텔 CPU의 instruction으로 확인했습니다.
https://www.felixcloutier.com/x86/syscall
SYSCALL — Fast System Call
SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX). (The WRMSR instruction ensures that the IA32_LSTAR MSR always contai
www.felixcloutier.com
%rax의 값에 따라 어떤 시스템콜이 불리는지는 아래에 잘 정리되어 있었습니다. (커널 버전 상이할 수 있음)
https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
Linux System Call Table for x86 64 · Ryan A. Chapman
Linux 4.7 (pulled from github.com/torvalds/linux on Jul 20 2016), x86_64 Note: 64-bit x86 uses syscall instead of interrupt 0x80. The result value will be in %rax To find the implementation of a system call, grep the kernel tree for SYSCALL_DEFINE.\?(sysca
blog.rchapman.org
i386
예전 버전인 i386에서는 syscall이라는 instruction 대신에 'int $0x80' (interrupt)라는 명령어를 통해 커널에게 넘겨줬다고 해요. 똑같이 glibc에서 i386을 찾다 보면 그렇게 구현이 된 것을 볼 수 있었습니다~
* sysdeps/unix/sysv/linux/i386/syscall.S
ENTRY (syscall)
PUSHARGS_6 /* Save register contents. */
_DOARGS_6(44) /* Load arguments. */
movl 20(%esp), %eax /* Load syscall number into %eax. */
ENTER_KERNEL /* Do the system call. */
POPARGS_6 /* Restore register contents. */
cmpl $-4095, %eax /* Check %eax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
ret /* Return to caller. */
PSEUDO_END (syscall)
* sysdeps/unix/sysv/linux/i386/sysdep.h
/* The original calling convention for system calls on Linux/i386 is
to use int $0x80. */
#if I386_USE_SYSENTER
# ifdef PIC
# define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
# else
# define ENTER_KERNEL call *_dl_sysinfo
# endif
#else
# define ENTER_KERNEL int $0x80
#endif
1. byeo_hello 호출 (모든 작업은 QEMU VM에서 수행)
root@bklee-qemu:~/practice# !g
gcc byeo_hello.c
byeo_hello.c: In function ‘main’:
byeo_hello.c:13:19: warning: implicit declaration of function ‘byeo_hello’ [-Wimplicit-function-declaration]
13 | int ret = byeo_hello(str, length);
| ^~~~~~~~~~
/usr/bin/ld: /tmp/ccb7wDha.o: in function `main':
byeo_hello.c:(.text+0xcd): undefined reference to `byeo_hello'
collect2: error: ld returned 1 exit status
syscall(439, str, length); 코드를 제거하고 byeo_hello(str, length);로 변경하였습니다. 그 뒤에 컴파일을 진행하면 위처럼 함수를 찾을 수 없다고 오류가 뜨네요.
2. glibc 수정하기
1) glibc source code 다운로드하기
git clone https://sourceware.org/git/glibc.git
2) glibc 컴파일해보기 (Skip 가능)
cd glibc
git checkout glibc-2.36
mkdir build
cd build
export glibc_install="$(pwd)/install"
../configure --prefix "$glibc_install"
make -j8
make -j8 install
make -j8의 시간이 꽤 걸립니다. i5-10210 기준(갤럭시 이온 노트북)으로 약 15분 소요됩니다.
3) 컴파일한 glibc로 C 컴파일 해보기 (Skip 가능)
qemu 내에서 아래와 같은 C코드를 하나 만들고, gcc option을 이용해 위에서 build 했던 glibc를 연결해 줍니다.
#include <stdio.h>
#include <string.h>
int main() {
char str[100] = "BYEO's user string glibc practice!";
int length = strlen(str);
int ret = byeo_hello(str,length);
printf("ret : %d, str: %s\n", ret, str);
return 0;
}
아래는 gcc 컴파일을 쉽게 하기 위하여 쉘스크립트로 저장했습니다.
#!/usr/bin/env bash
set -eux
export glibc_install="/root/glibc/build/install"
gcc -L "${glibc_install}/lib" \
-I "${glibc_install}/include" \
-Wl,--rpath="${glibc_install}/lib" \
-Wl,--dynamic-linker="${glibc_install}/lib/ld-linux-x86-64.so.2" \
-std=c11 \
-o byeo.out \
byeo_glibc.c
ldd ./byeo.out
./byeo.out
한 번 실행해 봅니다.
root@bklee-qemu:~/practice2# ./byeo_glibc.sh
+ export glibc_install=/root/glibc/build/install
+ glibc_install=/root/glibc/build/install
+ gcc -L /root/glibc/build/install/lib -I /root/glibc/build/install/include -Wl,--rpath=/root/glibc/build/install/lib -Wl,--dynamic-linker=/root/glibc/build/install/lib/ld-linux-x86-64.so.2 -std=c11 -o byeo.out byeo_glibc.c
byeo_glibc.c: In function ‘main’:
byeo_glibc.c:7:19: warning: implicit declaration of function ‘byeo_hello’ [-Wimplicit-function-declaration]
7 | int ret = byeo_hello(str,length);
| ^~~~~~~~~~
/usr/bin/ld: /tmp/cctfAVNQ.o: in function `main':
byeo_glibc.c:(.text+0xbb): undefined reference to `byeo_hello'
collect2: error: ld returned 1 exit status
당연히 glibc를 수정한 내용이 없으니 똑같은 에러가 발생합니다.
4) glibc 수정하기
glibc는 Kernel이 제공하는 시스템콜들에 대하여 wrapper역할을 하는 라이브러리입니다. 이제 하나씩 파일을 추가해서 바꿔봅시다.
* sysdeps/unix/sysv/linux/x86/byeo.c
#include <errno.h>
#include <sys/byeo.h>
#include <sysdep.h>
#include <sys/syscall.h>
#include <assert.h>
int
__byeo_hello(char *user_string, int strlen)
{
return INLINE_SYSCALL_CALL (__NR_byeo_hello, 2, user_string, strlen);
}
libc_hidden_def (__byeo_hello)
weak_alias (__byeo_hello, byeo_hello)
* sysdeps/unix/sysv/linux/x86/sys/byeo.h
#ifndef _SYS_BYEO_HELLO_H
#define _SYS_BYEO_HELLO_H
extern int byeo_hello (char *user_string, int strlen);
#endif
* sysdeps/unix/sysv/linux/x86/Makefile
sysdep_headers += sys/elf.h sys/perm.h sys/reg.h sys/vm86.h sys/debugreg.h sys/io.h sys/byeo.h
새로운 파일을 추가했으니 Makefile에 이를 인식할 수 있도록 헤더를 추가해 줍니다.
* sysdeps/unix/sysv/linux/syscall-names.list
...
bpf
break
breakpoint
brk
byeo_hello
cachectl
cacheflush
capget
...
해당 파일 내용에 우리의 시스템콜 'byeo_hello'를 추가해 줍니다. 알파벳 순서에 맞지 않게 넣으면 glibc 빌드할 때 에러가 발생합니다.
* sysdeps/unix/sysv/linux/syscalls.list
# File name Caller Syscall name Args Strong name Weak names
byeo_hello EXTRA byeo_hello i:VU byeo_hello
원하시는 인자가 다르다면, Args는 다른 시스템콜 참조해서 넣으시면 될 것 같습니다. 느낌상 {return type}:{Argument type}으로 보입니다.
* sysdeps/unix/sysv/linux/x86_64/64/arch-syscall.h
#define __NR_byeo_hello 449
5) glibc 컴파일
2)에서 했던 방법대로 glibc 컴파일을 다시 해줍니다. skip 하셨었다면 2)를 따라 해주세요.
make -j8 install
3. 실행하기
1) 유저 프로그램 코드 변경
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/byeo.h>
int main() {
char str[100] = "BYEO's user string glibc practice!";
int length = strlen(str);
int ret = byeo_hello(str,length);
printf("ret: %d, errno: %d, str: %s\n", ret, errno, str);
return 0;
}
우리가 개발한 sys/byeo.h 헤더를 include 해줍니다.
2) 유저 프로그램 컴파일 및 실
'2. glibc 수정하기 - 3) 컴파일한 glibc로 C 컴파일해보기' 내용에서 적혀있던 대로 gcc를 실행해 줍니다.
root@bklee-qemu:~/practice2# ./byeo_glibc.sh
+ export glibc_install=/root/glibc/build/install
+ glibc_install=/root/glibc/build/install
+ gcc -L /root/glibc/build/install/lib -I /root/glibc/build/install/include -Wl,--rpath=/root/glibc/build/install/lib -Wl,--dynamic-linker=/root/glibc/build/install/lib/ld-linux-x86-64.so.2 -std=c11 -o byeo.out byeo_glibc.c
+ ldd ./byeo.out
linux-vdso.so.1 (0x00007ffd02ac8000)
libc.so.6 => /root/glibc/build/install/lib/libc.so.6 (0x00007f499e168000)
/root/glibc/build/install/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f499e348000)
+ ./byeo.out
ret: 0, errno: 0, str: BYEO's user string glibc practice!
시스템콜의 반환 값도 0이고, errno도 0입니다!
잘 된 것을 확인할 수 있었습니다 ㅎㅎ
마무리
어떻게 보면 glibc에다가 단순히 syscall macro(449, ...) 함수를 추가하여 wrapping 해준 것 밖에 없긴 합니다. 그래도 조사하다 보니 별생각 없이 사용하던 그 흔한 printf 같은 친구들이 glibc 단에서 어떻게 변환되는지 알아볼 수 있는 기회가 되었네요. (참고로 printf는 glibc/stdio-common/printf.c에 있습니다.)
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
socket system call 2 (inet_create) (0) | 2024.03.31 |
---|---|
socket system call 1 (syscall_socket ~ __sock_create) (0) | 2024.03.31 |
Linux System Call 추가하기 (0) | 2024.03.16 |
VSCode에 Linux Kernel Source Code 환경 구성하기 (0) | 2024.03.12 |
Linux Kernel Compile + QEMU 테스트 (2) | 2024.03.02 |