Linux
/drivers/char/mem.c 드라이버 2차 분석
Jminu
2026. 1. 1. 18:04
기존
devlist[5] = {"zero", &zero_fops, FMODE_NOWAIT, 0666};
devlist[7] = {"full", &full_fops, 0, 0666};
에서 write_zero와 read_zero에 대해서 이전에 봤다.
간단하게 보자면,
zero_fops
#define write_zero write_null이고,
write_null 코드
421 static ssize_t write_null(struct file *file, const char __user *buf,
422 size_t count, loff_t *ppos)
423 {
424 return count;
425 }
- 걍 아무 동작도 하지않음.
read_zero 코드
480 static ssize_t read_zero(struct file *file, char __user *buf,
481 size_t count, loff_t *ppos)
482 {
483 size_t cleared = 0;
484
485 while (count) {
486 size_t chunk = min_t(size_t, count, PAGE_SIZE);
487 size_t left;
488
489 left = clear_user(buf + cleared, chunk); //유저로 보낼 buf clear: 내부에 memset으로 전부 0 초기화
490 if (unlikely(left)) {
491 cleared += (chunk - left);
492 if (!cleared)
493 return -EFAULT;
494 break;
495 }
496 cleared += chunk;
497 count -= chunk;
498
499 if (signal_pending(current))
500 break;
501 cond_resched();
502 }
503
504 return cleared;
505 }
- clear_user(buf + cleared, chunk)에 의해서 유저로 보낼 버퍼를 0으로 채운다.
- clear_user함수는 타고타고 들어가다보면, 결국 memset으로 특정 메모리 주소에 0을 채우는 걸 볼 수 있다.
#define __clear_user(addr, n) (memset((void __force *)addr, 0, n), 0)
full_fops
full_fops에서는
.write = write_full
.read_iter = read_iter_zero
로 구성된다.
winclude/linux/uaccess.hrite_full 코드
566 static ssize_t write_full(struct file *file, const char __user *buf,
567 size_t count, loff_t *ppos)
568 {
569 return -ENOSPC; // no space left on device:용량 없음
570 }
- 아무 동작안하고 그냥 '용량 없음'에러를 내뱉는다.
read_iter_zero 코드
456 static ssize_t read_iter_zero(struct kiocb *iocb, struct iov_iter *iter)
457 {
458 size_t written = 0;
459
460 while (iov_iter_count(iter)) {
461 size_t chunk = iov_iter_count(iter), n;
462
463 if (chunk > PAGE_SIZE)
464 chunk = PAGE_SIZE; /* Just for latency reasons */
465 n = iov_iter_zero(chunk, iter);
466 if (!n && iov_iter_count(iter))
467 return written ? written : -EFAULT;
468 written += n;
469 if (signal_pending(current))
470 return written ? written : -ERESTARTSYS;
471 if (!need_resched())
472 continue;
473 if (iocb->ki_flags & IOCB_NOWAIT)
474 return written ? written : -EAGAIN;
475 cond_resched();
476 }
477 return written;
478 }
- chunk = iov_iter_count(iter): 루프 한번에 처리해야할 청크(덩어리)
- PAGE_SIZE로 제한: 4KB씩 쪼개서 처리
- n = iov_iter_zero(chunk, iter): chunk만큼 0을 채워넣음
요약: 그냥 read_zero랑 동일하지만, chunk단위로 쪼개서 처리한다는 점이 다름
mem_fops
오늘의 main dish.
701 static const struct memdev {
702 const char *name;
703 const struct file_operations *fops;
704 fmode_t fmode;
705 umode_t mode;
706 } devlist[] = {
707 #ifdef CONFIG_DEVMEM
708 [DEVMEM_MINOR] = { "mem", &mem_fops, 0, 0 },
709 #endif
710 [3] = { "null", &null_fops, FMODE_NOWAIT, 0666 },
711 #ifdef CONFIG_DEVPORT
712 [4] = { "port", &port_fops, 0, 0 },
713 #endif
714 [5] = { "zero", &zero_fops, FMODE_NOWAIT, 0666 },
715 [7] = { "full", &full_fops, 0, 0666 },
716 [8] = { "random", &random_fops, FMODE_NOWAIT, 0666 },
717 [9] = { "urandom", &urandom_fops, FMODE_NOWAIT, 0666 },
718 #ifdef CONFIG_PRINTK
719 [11] = { "kmsg", &kmsg_fops, 0, 0644 },
720 #endif
mem_fops를 보자.
- DEVMEM_MINOR = 1
- read_mem
- write_mem
read_mem 코드 (일부 생략)
static ssize_t read_mem(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
phys_addr_t p = *ppos;
ssize_t read, sz;
void *ptr;
char *bounce;
int err;
...
...
#endif
bounce = kmalloc(PAGE_SIZE, GFP_KERNEL);
if (!bounce)
return -ENOMEM;
while (count > 0) {
unsigned long remaining;
int allowed, probe;
sz = size_inside_page(p, count);
err = -EPERM;
allowed = page_is_allowed(p >> PAGE_SHIFT);
if (!allowed)
goto failed;
err = -EFAULT;
if (allowed == 2) {
/* Show zeros for restricted memory. */
remaining = clear_user(buf, sz);
} else {
/*
* On ia64 if a page has been mapped somewhere as
* uncached, then it must also be accessed uncached
* by the kernel or data corruption may occur.
*/
ptr = xlate_dev_mem_ptr(p);
if (!ptr)
goto failed;
probe = copy_from_kernel_nofault(bounce, ptr, sz);
unxlate_dev_mem_ptr(p, ptr);
if (probe)
goto failed;
remaining = copy_to_user(buf, bounce, sz);
}
if (remaining)
goto failed;
buf += sz;
p += sz;
count -= sz;
read += sz;
if (should_stop_iteration())
break;
}
kfree(bounce);
*ppos += read;
return read;
failed:
kfree(bounce);
return err;
}
- phys_addr_t p = *ppos: ppos는 file offset, 물리주소 p에 대입. why???
- bounce = kmalloc(PAGE_SIZE, GFP_KERNEL): 버퍼 생성 (이후에 유저단으로 전송)
- allowed = page_is_allowed(p >> PAGE_SHIFT): 해당 메모리 주소에 접근 가능한가?
- ptr = xlate_dev_mem_ptr(p): p는 물리주소. xlate_dev_mem_ptr로 물리주소 p를 가상주소로 매핑
- probe = copy_from_kernel_nofault(bounce, ptr, sz): 유저로 보낼 bounce버퍼에 ptr이 가리키는 값 저장
- unxlate_dev_mem_ptr(p, ptr): 가상 주소 자원 해제
요약: mem파일에 read발생 시, 해당 메모리 주소에서의 값을 복사해서 유저단으로 보낸다.
write_mem 코드 (일부 생략)
static ssize_t write_mem(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
phys_addr_t p = *ppos;
ssize_t written, sz;
unsigned long copied;
void *ptr;
if (p != *ppos)
return -EFBIG;
if (!valid_phys_addr_range(p, count))
return -EFAULT;
written = 0;
...
...
}
#endif
while (count > 0) {
int allowed;
sz = size_inside_page(p, count);
allowed = page_is_allowed(p >> PAGE_SHIFT);
if (!allowed)
return -EPERM;
/* Skip actual writing when a page is marked as restricted. */
if (allowed == 1) {
/*
* On ia64 if a page has been mapped somewhere as
* uncached, then it must also be accessed uncached
* by the kernel or data corruption may occur.
*/
ptr = xlate_dev_mem_ptr(p);
if (!ptr) {
if (written)
break;
return -EFAULT;
}
copied = copy_from_user(ptr, buf, sz);
unxlate_dev_mem_ptr(p, ptr);
if (copied) {
written += sz - copied;
if (written)
break;
return -EFAULT;
}
}
buf += sz;
p += sz;
count -= sz;
written += sz;
if (should_stop_iteration())
break;
}
*ppos += written;
return written;
}
- read_mem과 동작이 정확히 반대
- copied = copy_from_user: 유저에서의 buf를 커널에 ptr주소에 복사
read_mem과 write_mem의 다른 부분
- read_mem에서는 copy_from_kernel_nofault를 사용하고 write_mem에서는 일반적인 copy_from_user를 사용.
- copy_from_user: 유저->커널 복사 (드라이버 코드에서 많이 보임)
- copy_from_kernel_nofault
copy_from_kernel_nofault
long copy_from_kernel_nofault(void *dst, const void *src, size_t size)
{
unsigned long align = 0;
if (!IS_ENABLED(CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS))
align = (unsigned long)dst | (unsigned long)src;
if (!copy_from_kernel_nofault_allowed(src, size))
return -ERANGE;
pagefault_disable();
if (!(align & 7))
copy_from_kernel_nofault_loop(dst, src, size, u64, Efault);
if (!(align & 3))
copy_from_kernel_nofault_loop(dst, src, size, u32, Efault);
if (!(align & 1))
copy_from_kernel_nofault_loop(dst, src, size, u16, Efault);
copy_from_kernel_nofault_loop(dst, src, size, u8, Efault);
pagefault_enable();
return 0;
Efault:
pagefault_enable();
return -EFAULT;
}
- if (!copy_from_kernel_nofault_allowed(src, size)): 데이터 가져올 주소에 접근이 가능한지 체크
- pagefault_disable(), pagefault_enable(): 페이지 폴트가 발생해도 무시하기
- align코드: 메모리 정렬 (최근 커널에 패치 보낸 내용과 관련있음)
phys_addr_t p = *ppos
typedef u64 phys_addr_t;
- u64: 64비트 정수자료형
- phys_addr_t p = *ppos: 파일 오프셋을 물리주소에 입력
파일 오프셋이 어떻게 물리주소로 치환될 수 있나?
/dev/mem은 메모리 파일
메모리를 일종의 파일로 생각한다.
만약 메모리가 8GB면 8GB파일이라고 생각함
만약 파일의 시작지점이 0번지이고 현재 offset이 1024면
현재 offset의 물리주소는 당연히 0 + 1024가 된다.
파일에서 n바이트 떨어진 곳을 읽으면, 물리 주소 n번지를 읽게된다.