Byeo

추가한 System Call을 함수 명으로 호출하기 (glibc) 본문

프로그래밍 (Programming)/네트워크 스택

추가한 System Call을 함수 명으로 호출하기 (glibc)

BKlee 2024. 3. 22. 23:19
반응형

https://sys.readthedocs.io/en/latest/doc/07_calling_system_calls.html

 

 지난 포스트에서 추가한 리눅스 시스템콜을 사용하기 위해서는 위에서 기술된 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개까지 사용할 수 있네요.)

https://man7.org/linux/man-pages/man2/syscall.2.html

 

그리고 어셈블리 중간에 보이는 '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 수정하기

참고 자료: stack overflow[각주:1]

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에 있습니다.)

반응형
Comments