기본적으로 운영체제의 메모리와 사용자의 메모리 영역은 분리되어 있다. 주로 보안의 문제로 사용자는 커널 영역의 메모리에 접근할 수 없다. (사용자가 임의로 커널 메모리에 접근하여 커널의 데이터를 훼손하면 시스템에 치명적인 에러를 발생할 수 있다) 그러나 알다시피 우리는 분명 커널로부터 데이터를 받고 또 줄 수 있다. 직접적인 접근을 막고 대신 커널과 유저 사이의 통신 API 를 만들어놨다. 이번 장에서는 리눅스 커널과 유저 사이의 데이터 교환 방법을 살펴 보겠다.
함수명 | 설명 |
---|---|
get_user() | 유저 메모리에서 커널 메모리로 단순 변수값을 가져옴. |
put_user() | 커널 메모리에서 유저 메모리로 단순 변수값을 내보냄. |
clear_user() | 유저 메모리 내용을 비움. |
copy_to_user() | 커널 메모리를 유저 메모리로 데이터의 덩어리를 복사함. |
copy_from_user() | 유저 메모리를 커널 메모리로 데이터의 덩어리를 복사함. |
strnlen_user() | 유저 메모리 버퍼의 크기를 가져옴. |
strncpy_from_user() | 유저 메모리 문자열을 커널 메모리로 복사함. |
위 그림은 앞선 함수들이 어떤 방향으로 데이터를 전달하는지를 잘 보여준다.
메모리 주소는 컴퓨터 아키텍쳐마다 조금씩 다르지만 일반적으로 32bit
CPU 에서 메모리 주소는 32bit
이고 64bit
CPU에서 메모리 주소는 64bit
이다. 필자는 x86
을 사용 중이므로 x86
CPU 에서 메모리 주소를 커널에서 어떻게 관리하는지 살펴보려 한다.
32 bit
메모리 주소
arch/x86/Kconfig
파일의 내용을 가져와 보았다. 다음과 같이 Memory Split
을 설정하는 부분이 있다. 기본 설정은 VMSPLIT_3G
이고 이는 3GiB
를 유저 영역으로 1GiB
를 커널 영역으로 쪼갠다는 뜻이다. 메모리 주소 0xC0000000
기준으로 상위 주소는 커널이, 하위 주소는 유저가 나눠 가지게 되고
최종적으로 위 삽화와 같은 형태를 이룬다.
64 bit
메모리 주소 64 비트 주소는 32 비트에 비해 살짝 복잡하다.
0x00000000000000
~ 0x00007FFFFFFFFFFF
까지는 사용자 가상 메모리 공간이다. 그리고 그 뒤로 0x0000800000000000
부터 0xFFFF7FFFFFFFFFFF
까지는 커널 맵핑에 사용되는 비정형적 가상 메모리 주소 공간이다. 참고로 0xFFFF7FFFFFFFFFFF
는 그 크기가 무려 16MB TB
에 달한다. 1024 x 1024 x 16 TiB
이다. 얼마나 큰지 예상이 되는가?
다시 아래에서부터 오프셋을 잡아 -128 TB ~ -0 byte
까지는 커널이 사용하는 메모리 주소 공간이다.
user copy
테스트 코드 분석 리눅스 커널 소스에는 lib/test_user_copy.c
라는 이름의 user copy
를 테스트 해볼 수 있는 샘플 코드를 제공한다.
맨 위부터 순서대로 쭉 내려가면서 분석해보겠다.
가장 먼저 TEST_U64
매크로에 대한 조건부 컴파일 매크로가 정의되어 있는데 BITS_PER_LONG
이 64bit
라면 TEST_U64
를 정의하여 64bit
데이터 타입에 대해서도 테스트가 올바르게 수행되게 만들어준다.
그 바로 아래에 정의되어 있는 매크로는 표현식의 값을 테스트 하는 매크로다. 인자로 전달된 condition
이 0
이 아니라면 (다른 말로 참이라면) 경고를 출력한다.
원래 탭 사이즈를 8 칸
으로 해야 하는데 이후의 코드가 너무 크게 벌어져서 일부러 4 칸
을 사용했다. 리눅스 커널 코딩 양식에 따르면 8개의 공백을 가지는 탭을 사용하는 것이 옳다.
맨 위의 kmalloc
함수는 PAGE_SIZE * 2
크기 만큼의 메모리를 할당한다.
user_addr
은 vm_mmap
함수에 의해서 할당되는 유저 영역의 메모리 주소이다. 크기는 kmem
과 동일하다. 이어지는 if 문
은 user_addr
이 TASK_SIZE
를 넘는지 확인한다. TASK_SIZE
를 넘어간다는 것은 곧 커널 영역음 침범한다는 것이므로 적절한 에러 처리를 해준다.
위 아래로 보이는 pr_info
함수는 필자가 디버깅을 위해 끼워넣은 구문이다. 독자 역시 위와 같은 코드를 추가해서 어떤 결과과 나오는지 확인해보는 것을 추천한다.
각 행을 순서대로 설명하면 아래와 같다:
kmem
전체(PAGE_SIZE * 2
) 를 0x3a
비트 구조로 초기화한다.usermem
(이는 앞서 초기화한 user_addr
이다) 메모리 영역의 절반(PAGE_SIZE
) 을 kmem
메모리로 덮어 쓴다.kmem
의 절반의 메모리를 0x00
비트 구조로 초기화한다.2번
에서 kmem
으로 덮어쓴 usermem
으로 다시 kmem
의 절반에 해당하는 메모리를 덮어쓴다.kmem
과 kmem
에서 PAGE_SIZE
만큼 떨어진 메모리(kmem
의 절반) 를 PAGE_SIZE
만큼 비교한다.성공적으로 유저 영역에서 데이터를 쓰고 읽는데 성공했다면 아무런 메세지도 출력되지 않을 것이다. (정상)
test_legit()
매크로 정의test_legit
매크로는 위에서 보았던 메모리 테스트 코드의 연장이다. test_legit
은 유저 영역에서 커널 영역으로 데이터를 복사, 그 반대의 명령을 수행하면서 발생하는 에러를 검사한다.
put_user
명령을 수행하고, 실패 시 두 번째 인자로 전달된 문자열을 출력한다. put_user
명령을 통해 val_##size
그러니까 check
의 값을 usermem
에 대입한다.val_##size
변수를 0
으로 초기화한다.val_##size
에 저장한다.check
와 val_##size
서로 다른지 확인한다.test_legit
매크로 테스트
위에서 본 코드를 이해했다면 어떻게 치환될지 쉽게 이해할 수 있을 것이다. 필자는 x86_64
CPU 를 사용하기 때문에 가장 아래의 test_legit(u64, ...)
구문도 실행될 것이다.
필자는 다음과 같이 modules_test/
폴더를 만들어서 lib/test_user_copy.c
코드를 복사했다. 이미 빌드를 해서 모듈 파일이 생성되어 있다.
Makefile
Makefile
은 위와 같이 작성했다. 코드를 완전 망쳐놓은게 아니라면 make
명령어어 입력으로 모듈을 빌드할 수 있을 것이다:
약간의 경고가 나타나긴 했으나 중요한 사항은 아니다.
빌드에 성공했다면 아래의 명령어를 입력하여 모듈을 삽입할 수 있다.
sudo insmod test_user_copy.ko
journalctl
위와 같이 tests passed.
문구가 나오면 정상적으로 실행된 것이다. 위에는 필자가 디버깅 용도로 추가한 pr_info
구문의 실행 결과가 나와있다.
[책] 리눅스 커널 소스 해설: 기초 입문 (정재준 저)
[사이트] https://www.kernel.org/doc/htmldocs/kernel-api/API---copy-from-user.html
[이미지] https://developer.ibm.com/technologies/linux/articles/l-kernel-memory-access/
[이미지] https://www.programmersought.com/article/17903543345/
[사이트] https://www.kernel.org/doc/html/latest/x86/x86_64/mm.html