Linux Tutorial #18 user copy 테스트

문연수·2021년 6월 3일
0

Linux Tutorial

목록 보기
19/25

기본적으로 운영체제의 메모리와 사용자의 메모리 영역은 분리되어 있다. 주로 보안의 문제로 사용자는 커널 영역의 메모리에 접근할 수 없다. (사용자가 임의로 커널 메모리에 접근하여 커널의 데이터를 훼손하면 시스템에 치명적인 에러를 발생할 수 있다) 그러나 알다시피 우리는 분명 커널로부터 데이터를 받고 또 줄 수 있다. 직접적인 접근을 막고 대신 커널과 유저 사이의 통신 API 를 만들어놨다. 이번 장에서는 리눅스 커널과 유저 사이의 데이터 교환 방법을 살펴 보겠다.

1. Kernel ⇆ User 메모리 복사 API

함수명설명
get_user()유저 메모리에서 커널 메모리로 단순 변수값을 가져옴.
put_user()커널 메모리에서 유저 메모리로 단순 변수값을 내보냄.
clear_user()유저 메모리 내용을 비움.
copy_to_user()커널 메모리를 유저 메모리로 데이터의 덩어리를 복사함.
copy_from_user()유저 메모리를 커널 메모리로 데이터의 덩어리를 복사함.
strnlen_user()유저 메모리 버퍼의 크기를 가져옴.
strncpy_from_user()유저 메모리 문자열을 커널 메모리로 복사함.

위 그림은 앞선 함수들이 어떤 방향으로 데이터를 전달하는지를 잘 보여준다.

2. 커널 메모리 주소의 이해

메모리 주소는 컴퓨터 아키텍쳐마다 조금씩 다르지만 일반적으로 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 까지는 커널이 사용하는 메모리 주소 공간이다.

3. user copy 테스트 코드 분석

리눅스 커널 소스에는 lib/test_user_copy.c 라는 이름의 user copy 를 테스트 해볼 수 있는 샘플 코드를 제공한다.

맨 위부터 순서대로 쭉 내려가면서 분석해보겠다.

1. 매크로 정의

가장 먼저 TEST_U64 매크로에 대한 조건부 컴파일 매크로가 정의되어 있는데 BITS_PER_LONG64bit 라면 TEST_U64 를 정의하여 64bit 데이터 타입에 대해서도 테스트가 올바르게 수행되게 만들어준다.

그 바로 아래에 정의되어 있는 매크로는 표현식의 값을 테스트 하는 매크로다. 인자로 전달된 condition0 이 아니라면 (다른 말로 참이라면) 경고를 출력한다.

추가

원래 탭 사이즈를 8 칸으로 해야 하는데 이후의 코드가 너무 크게 벌어져서 일부러 4 칸 을 사용했다. 리눅스 커널 코딩 양식에 따르면 8개의 공백을 가지는 탭을 사용하는 것이 옳다.

2. 메모리 할당

맨 위의 kmalloc 함수는 PAGE_SIZE * 2 크기 만큼의 메모리를 할당한다.
user_addrvm_mmap 함수에 의해서 할당되는 유저 영역의 메모리 주소이다. 크기는 kmem 과 동일하다. 이어지는 if 문user_addrTASK_SIZE 를 넘는지 확인한다. TASK_SIZE 를 넘어간다는 것은 곧 커널 영역음 침범한다는 것이므로 적절한 에러 처리를 해준다.

추가

위 아래로 보이는 pr_info 함수는 필자가 디버깅을 위해 끼워넣은 구문이다. 독자 역시 위와 같은 코드를 추가해서 어떤 결과과 나오는지 확인해보는 것을 추천한다.

3. 메모리 테스트 코드

각 행을 순서대로 설명하면 아래와 같다:

  1. kmem 전체(PAGE_SIZE * 2) 를 0x3a 비트 구조로 초기화한다.
  2. usermem (이는 앞서 초기화한 user_addr 이다) 메모리 영역의 절반(PAGE_SIZE) 을 kmem 메모리로 덮어 쓴다.
  3. 다시 kmem 의 절반의 메모리를 0x00 비트 구조로 초기화한다.
  4. 2번 에서 kmem 으로 덮어쓴 usermem 으로 다시 kmem 의 절반에 해당하는 메모리를 덮어쓴다.
  5. kmemkmem 에서 PAGE_SIZE 만큼 떨어진 메모리(kmem 의 절반) 를 PAGE_SIZE 만큼 비교한다.

성공적으로 유저 영역에서 데이터를 쓰고 읽는데 성공했다면 아무런 메세지도 출력되지 않을 것이다. (정상)

4. test_legit() 매크로 정의

test_legit 매크로는 위에서 보았던 메모리 테스트 코드의 연장이다. test_legit 은 유저 영역에서 커널 영역으로 데이터를 복사, 그 반대의 명령을 수행하면서 발생하는 에러를 검사한다.

  1. 가장 먼저 첫 번째 인자로 들어온 크기의 자료형을 선택하여 두 번째 인자를 대입한다.
  2. put_user 명령을 수행하고, 실패 시 두 번째 인자로 전달된 문자열을 출력한다. put_user 명령을 통해 val_##size 그러니까 check 의 값을 usermem 에 대입한다.
  3. val_##size 변수를 0 으로 초기화한다.
  4. 다시 유저 영역으로부터 데이터를 읽어와 val_##size 에 저장한다.
  5. checkval_##size 서로 다른지 확인한다.
  6. 서로 다르다면 두 변수의 값을 출력한다.

5. test_legit 매크로 테스트

위에서 본 코드를 이해했다면 어떻게 치환될지 쉽게 이해할 수 있을 것이다. 필자는 x86_64 CPU 를 사용하기 때문에 가장 아래의 test_legit(u64, ...) 구문도 실행될 것이다.

4. 모듈 삽입 후 테스트

필자는 다음과 같이 modules_test/ 폴더를 만들어서 lib/test_user_copy.c 코드를 복사했다. 이미 빌드를 해서 모듈 파일이 생성되어 있다.

1. Makefile

Makefile 은 위와 같이 작성했다. 코드를 완전 망쳐놓은게 아니라면 make 명령어어 입력으로 모듈을 빌드할 수 있을 것이다:

약간의 경고가 나타나긴 했으나 중요한 사항은 아니다.

2. 모듈 삽입하기

빌드에 성공했다면 아래의 명령어를 입력하여 모듈을 삽입할 수 있다.

sudo insmod test_user_copy.ko

3. 실행 결과 확인

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

profile
2000.11.30

0개의 댓글