Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

119199
1107
135847


[리눅스커널] 가상파일시스템: 파일 속성을 읽는 stat 시스템 콜 처리 과정 분석하기 13. 가상 파일 시스템

유저 공간에서 세부 파일 속성을 파악하려면 어떤 함수를 호출해야 할까요? stat() 함수를 호출하면 가상 파일시스템에서 아이노드 객체에 접근해 상세 파일 속성 정보를 읽습니다.
 
이번 소절에서는 stat() 함수를 호출하면 가상 파일시스템에서 어떤 흐름으로 아이노드 객체에 접근하는지 살펴보겠습니다. 

stat 시스템 콜 처리 과정 확인하기

먼저 stat() 함수를 호출하는 유저 어플리케이션 코드를 소개합니다.  
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <unistd.h>
04 #include <sys/types.h>
05 #include <sys/stat.h>
06 #include <signal.h>
07 #include <string.h>
08 #include <fcntl.h>
09 int main() 
10 {
11 struct stat fileinfo;
12 char fname[BUFF_SIZE] = {0,};
13
14 strcpy(fname, "/home/pi/sample_text.text");
15
16 printf("fname[%s] \n", fname);
17
18 if(stat(fname, &fileinfo)) {
19 printf("Unable to stat %s \n", fname);
20 exit(1);
21 }
22
23 return 0;
24 }

먼저 18번째 줄 코드를 보면 stat() 함수를 호출합니다.
첫 번째 인자로 파일 경로가 포함된 파일 이름인 "/home/pi/sample_text.text"으로 지정하고 두 번째 인자로 파일 속성 정보를 표현하는 struct stat 구조체 변수인 fileinfo를 전달합니다. stat() 함수가 실행되면 fileinfo에 파일 속성 정보가 업데이트됩니다.

sys_fstat64() 함수 세부 코드 분석하기

유저 공간에서 stat() 함수를 호출하면 실행하는 시스템 콜 핸들러 함수는 무엇일까요?
다음 코드와 같이 sys_fstat64() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 SYSCALL_DEFINE2(fstat64, unsigned long, fd, struct stat64 __user *, statbuf)
02 {
03 struct kstat stat;
04 int error = vfs_fstat(fd, &stat);
05
06 if (!error)
07 error = cp_new_stat64(&stat, statbuf);
08
09 return error;
10 }

sys_fstat64() 함수는 크게 2개 동작으로 분류할 수 있습니다.

1단계: 아이노드 속성 정보 읽기

vfs_fstat() 함수를 호출해서 아이노드에 저장된 파일 속성 정보를 읽습니다. 이 과정에서 다음 함수를 호출합니다.
vfs_fstat()
vfs_statx_fd()
vfs_getattr() 
vfs_getattr_nosec() 
generic_fillattr() 

2단계: 유저 공간에 파일 정보 복사

cp_new_stat() 함수를 실행해서 파일 속성 정보를 유저 공간(struct stat 구조체)에 써줍니다. 

이어서 각 단계별로 실행하는 함수 소스 코드를 분석해볼까요?

1단계: 아이노드 속성 정보를 읽는 과정 분석하기

먼저 vfs_fstat() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/include/linux/fs.h]
static inline int vfs_fstat(int fd, struct kstat *stat)
{
return vfs_statx_fd(fd, stat, STATX_BASIC_STATS, 0);
}

vfs_fstat() 함수는 인라인 함수인데 단지 인자를 추가해서 vfs_statx_fd() 함수를 호출합니다. 3번째 인자로 STATX_BASIC_STATS 플래그를 지정합니다.

다음 vfs_statx_fd() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 int vfs_statx_fd(unsigned int fd, struct kstat *stat,
02 u32 request_mask, unsigned int query_flags)
03 {
04 struct fd f;
05 int error = -EBADF;
06
07 if (query_flags & ~KSTAT_QUERY_FLAGS)
08 return -EINVAL;
09
10 f = fdget_raw(fd);
11 if (f.file) {
12 error = vfs_getattr(&f.file->f_path, stat,
13     request_mask, query_flags);
14 fdput(f);
15 }
16 return error;
17 }
EXPORT_SYMBOL(vfs_statx_fd);

10번째 줄 코드를 보겠습니다.
10 f = fdget_raw(fd);

정수형인 파일스크립터인 fd 인자로 파일 객체를 읽는 동작입니다.

다음 11~13번째 줄 코드를 분석합니다.
11 if (f.file) {
12 error = vfs_getattr(&f.file->f_path, stat,
13     request_mask, query_flags);

11번째 줄 코드에서 파일 객체를 제대로 읽었는지 확인합니다.
파일 객체 주소가 유효한 경우 12번째 줄 코드와 같이 vfs_getattr() 함수를 호출합니다. 

이어서 vfs_getattr() 함수를 분석하겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 int vfs_getattr(const struct path *path, struct kstat *stat,
02 u32 request_mask, unsigned int query_flags)
03 {
04 int retval;
05
06 retval = security_inode_getattr(path);
07 if (retval)
08 return retval;
09 return vfs_getattr_nosec(path, stat, request_mask, query_flags);
10 }

09번째 줄 코드와 같이 vfs_getattr_nosec() 함수를 호출합니다. 

다음 vfs_getattr_nosec() 함수 코드를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 int vfs_getattr_nosec(const struct path *path, struct kstat *stat,
02       u32 request_mask, unsigned int query_flags)
03 {
04 struct inode *inode = d_backing_inode(path->dentry);
05
06 memset(stat, 0, sizeof(*stat));
07 stat->result_mask |= STATX_BASIC_STATS;
08 request_mask &= STATX_ALL;
09 query_flags &= KSTAT_QUERY_FLAGS;
10 if (inode->i_op->getattr)
11 return inode->i_op->getattr(path, stat, request_mask,
12     query_flags);
13
14 generic_fillattr(inode, stat);
15 return 0;
16 }

4번째 줄 코드에서 덴트리 객체에 저장된 아이노드 객체 정보를 로딩합니다.
4 struct inode *inode = d_backing_inode(path->dentry);

이후 6번째 줄과 같이 stat 포인터를 0으로 초기화합니다.

10번째 줄에서는 아이노드 함수 오퍼레이션으로 getattr 필드에 함수가 지정돼 있으면 지정된 함수를 호출합니다.
10 if (inode->i_op->getattr)
11 return inode->i_op->getattr(path, stat, request_mask,
12     query_flags);

이번 소절에서 stat 함수를 호출할 때 지정한 파일 이름은  "/home/pi/sample_text.text" 파일입니다. 이 파일은 저장매체에 저장되므로 ext4 파일시스템에서 관리합니다. 이 기준으로 보면 11번째 줄 코드에서는 ext4_file_getattr() 함수를 호출합니다.

참고로 라즈비안에서 ext4와 proc 파일시스템에서 파일을 관리하는 아이노드 함수 오퍼레이션은 다음과 같습니다.
  (static struct inode_operations) ext4_file_inode_operations = (
    (struct dentry * (*)()) lookup = 0x0 = ,
    (char * (*)()) get_link = 0x0 = ,
    (int (*)()) permission = 0x0 = ,
    (struct posix_acl * (*)()) get_acl = 0x8036B9E4 = ext4_get_acl,
    (int (*)()) readlink = 0x0 = ,
    (int (*)()) create = 0x0 = ,
    (int (*)()) link = 0x0 = ,
    (int (*)()) unlink = 0x0 = ,
    (int (*)()) symlink = 0x0 = ,
    (int (*)()) mkdir = 0x0 = ,
    (int (*)()) rmdir = 0x0 = ,
    (int (*)()) mknod = 0x0 = ,
    (int (*)()) rename = 0x0 = ,
    (int (*)()) setattr = 0x80334BB4 = ext4_setattr,
    (int (*)()) getattr = 0x8032F9C8 = ext4_file_getattr,
    (ssize_t (*)()) listxattr = 0x80368D54 = ext4_listxattr,
    (int (*)()) fiemap = 0x8031A6E0 = ext4_fiemap,
    (int (*)()) update_time = 0x0 = ,
    (int (*)()) atomic_open = 0x0 = ,
    (int (*)()) tmpfile = 0x0 = ,
    (int (*)()) set_acl = 0x8036BC44 = ext4_set_acl)

다음 proc 파일시스템 기준 아이노드 함수 오퍼레이션 선언부입니다.
  (static struct inode_operations) proc_file_inode_operations = (
    (struct dentry * (*)()) lookup = 0x0 = ,
    (char * (*)()) get_link = 0x0 = ,
    (int (*)()) permission = 0x0 = ,
    (struct posix_acl * (*)()) get_acl = 0x0 = ,
    (int (*)()) readlink = 0x0 = ,
    (int (*)()) create = 0x0 = ,
    (int (*)()) link = 0x0 = ,
    (int (*)()) unlink = 0x0 = ,
    (int (*)()) symlink = 0x0 = ,
    (int (*)()) mkdir = 0x0 = ,
    (int (*)()) rmdir = 0x0 = ,
    (int (*)()) mknod = 0x0 = ,
    (int (*)()) rename = 0x0 = ,
    (int (*)()) setattr = 0x802F3CA0 = proc_notify_change,
    (int (*)()) getattr = 0x0 = ,
    (ssize_t (*)()) listxattr = 0x0 = ,
    (int (*)()) fiemap = 0x0 = ,
    (int (*)()) update_time = 0x0 = ,
    (int (*)()) atomic_open = 0x0 = ,
    (int (*)()) tmpfile = 0x0 = ,
    (int (*)()) set_acl = 0x0 = )

ext4 파일시스템 내 아이노드 함수 오퍼레이션인 경우 getattr 필드에 지정된 ext4_file_getattr() 함수 호출로 파일 속성 정보를 추가로 읽습니다. 대신 proc 파일시스템 아이노드 오퍼레이션으로 getattr 필드는 0x0으로 설정돼 있습니다. 이 경우 10~13번째 줄 코드는 실행하지 않습니다.

이후 ext4_file_getattr() 함수에서 ext4_getattr() 함수를 호출해 파일 속성 정보를 읽습니다. 이후 ext4_getattr() 함수에서 generic_fillattr() 함수를 호출합니다.

다음 generic_fillattr() 함수 코드를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
1 void generic_fillattr(struct inode *inode, struct kstat *stat)
2 {
3 stat->dev = inode->i_sb->s_dev;
4 stat->ino = inode->i_ino;
5 stat->mode = inode->i_mode;
6 stat->nlink = inode->i_nlink;
7 stat->uid = inode->i_uid;
8 stat->gid = inode->i_gid;
9 stat->rdev = inode->i_rdev;
10 stat->size = i_size_read(inode);
11 stat->atime = inode->i_atime;
12 stat->mtime = inode->i_mtime;
13 stat->ctime = inode->i_ctime;
14 stat->blksize = i_blocksize(inode);
15 stat->blocks = inode->i_blocks;
16
17 if (IS_NOATIME(inode))
18 stat->result_mask &= ~STATX_ATIME;
19 if (IS_AUTOMOUNT(inode))
20 stat->attributes |= STATX_ATTR_AUTOMOUNT;
21 }
EXPORT_SYMBOL(generic_fillattr);

파일 속성 정보를 채우는 핵심 루틴입니다. 각각 아이노드 필드들을 stat 이란 구조체 필드에 저장합니다. 

2단계: 유저 공간에 파일 정보 복사하는 과정 분석하기

파일 속성 정보를 유저 공간에 전달하는 단계 소스 코드를 분석할 차례입니다.
이번에는 sys_fstat64() 함수 분석으로 되돌아가서 2단계 코드를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/fs/stat.c]
01 SYSCALL_DEFINE2(fstat64, unsigned long, fd, struct stat64 __user *, statbuf)
02 {
03 struct kstat stat;
04 int error = vfs_fstat(fd, &stat);
05
06 if (!error)
07 error = cp_new_stat64(&stat, statbuf);
08
09 return error;
10 }

04번째 줄 코드와 같이 vfs_fstat() 함수를 호출해 파일속성 정보를 읽고 난 다음 07번째 줄 코드와 같이 cp_new_stat64() 함수를 호출합니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/source/fs/stat.c]
01 static long cp_new_stat64(struct kstat *stat, struct stat64 __user *statbuf)
02 {
03 struct stat64 tmp;
...
04 tmp.st_atime = stat->atime.tv_sec;
05 tmp.st_atime_nsec = stat->atime.tv_nsec;
06 tmp.st_mtime = stat->mtime.tv_sec;
07 tmp.st_mtime_nsec = stat->mtime.tv_nsec;
08 tmp.st_ctime = stat->ctime.tv_sec;
09 tmp.st_ctime_nsec = stat->ctime.tv_nsec;
10 tmp.st_size = stat->size;
11 tmp.st_blocks = stat->blocks;
12 tmp.st_blksize = stat->blksize;
13 return copy_to_user(statbuf,&tmp,sizeof(tmp)) ? -EFAULT : 0;
14 }

cp_new_stat64() 함수 분석에 앞서 함수 인자를 살펴보겠습니다.
struct kstat *stat 구조체는 가상 파일시스템을 통해 업데이트된 파일 속성 정보가 저장돼 있습니다. struct stat64 구조체는 유저 공간에서 파일 속성을 표현합니다. 

이어서 코드를 분석해볼까요?
04~12번째 줄 코드는 struct kstat 구조체에 저장된 파일 속성 정보를 struct stat 구조체로 복사합니다. 

다음 13번째 줄 코드와 같이 copy_to_user() 함수를 호출해서 &tmp 구조체에 저장된 데이터를 유저 공간 메모리 주소인 statbuf에 복사합니다.

여기까지 유저 공간에서 stat() 함수를 호출하면 커널 가상 파일시스템 계층을 통해 파일 속성 정보를 읽는 과정을 살펴봤습니다. 코드 분석으로 다음 내용을 알게 됐습니다.

    유저 공간에서 파일 속성 정보를 읽는 stat 함수를 호출하면 아이노드 객체에 있는 
    필드를 읽어 저장한다.
  
이어서 다음 절에는 덴트리 객체를 살펴보겠습니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)


Reference(가상 파일시스템)

가상 파일시스템 소개
파일 객체
파일 객체 함수 오퍼레이션 동작
프로세스는 파일객체 자료구조를 어떻게 관리할까?
슈퍼블록 객체
아이노드 객체
덴트리 객체
가상 파일시스템 디버깅



핑백

덧글

  • 영악한 얼음여왕 2020/03/01 15:06 # 답글

    안녕하세요 커널을 공부하고 있는 학생입니다. 커널 안에서 라이브러리 함수인 realpath()함수를 사용하고자 커널에 붙여넣으니 getcwd(), readlink(), lstat() 이 함수 3가지가 시스템콜로 되어있어 사용이 불가하더군요.. 그래서 수정한 결과 getcwd() 시스템콜을 함수로 만들어 사용하는데에 성공했습니다. 그러나 lstat(), readlink() 이 두 시스템콜은 계속 오류를 발생시킵니다. 저의 생각으로는 커널에서 문자열로 입력된 filename과 __user *로 되어있는 매개변수들이 호환이 맞지 않고, 반환값이 copy_to_user() 이기 때문이라 생각하고 코드를 수정해봤으나 부팅조차 안되었습니다. 어떻게 하면 이 시스템 콜을 커널에서 사용할 수 있을까요?
댓글 입력 영역