LIM(Linux Integrity Module) -1-

LIM (Linux Integrity Module)  -1-

 

다양한 공격으로부터 보호받아 신뢰할 수 있는 시스템을 구동하기 위해 사용되는 모듈이다.

  • Remote Attacks
    • trojan 같은 공격자의 악의적인 코드를 사용자가 구동시키게 하거나 시스템 소프트으ㅞ어의 또는 감염하게 하여 원격 소프트웨어 공격으로
    • 가장 일반적인 공격은 원격 소프트웨어 공격이다. 공격자가 사용자를 속여 공격자의 악성 코드 (트로이 목마)를 실행하거나 공격자가 시스템 소프트웨어의 취약점을 악용하기 위해 악성 데이터를 전송하도록 시도한다. (injection, overflow)
  • Local Attacks
    • 로컬 공격은 공격자가 악의적 인 내부자 (운영자)와 같은 시스템에 물리적으로 액세스하거나 시스템을 도용 한 경우를 가정한다.
    • 로컬 공격은 오프라인 공격과 같은 소프트웨어 기반 일 수도 있고 단순한 JTAG 메모리 프로브와 같은 하드웨어 기반 일 수도 있고 민감한 데이터를 읽기 위해 칩을 분리하는 매우 정교하고 비싼 공격 일 수도 있다.
    • 가장 간단하고 가장 일반적인 로컬 공격은 대체 운영 체제가 CD 또는 USB 드라이브에서 부팅되는 오프라인 공격이며이 운영 체제는 공격자가 대상 시스템을 수정하는 데 사용된다.
    • 오프라인 공격은 일반적으로 기존 암호를 해독하거나 알려진 암호를 삽입하는 것으로 공격자가 단순히 로그인 할 수 있다.
    • 보다 정교한 오프라인 공격의 경우 악의적 인 코드가 삽입되어 나중에 은행 비밀번호와 같은 중요한 데이터를 캡처 할 수 있다.

 

Integrity 서브 시스템

커널에서 CONFIG_INTEGRITY 커널 옵션을 사용하여 다음 서브시스템들을 사용할 수 있다.

  • IMA
    • IMA-Appraisal
    • IMA-Appraisal-Directory-Extension
    • IMA-Appraisal-Signature-Extension
  • EVM
  • Trusted and Encrypted Keys

 

Application

  • Trousers
    • 커널에 기본 TPM 장치 드라이버가 포함되어 있고 커널에 키 관리를 위해 TPM을 직접 사용하는 신뢰할 수있는 키가 포함되어 있다. 그렇지만 Trouser를 사용하는 경우 TPM 초기화, 관리 및 사용을위한 표준 준수 TPM 액세스 라이브러리 및 관련 TPM 유틸리티를 제공한다.
  • OpenPTS
    • OpenPTS는 Trousers 라이브러리를 사용하여 TPM 및 IMA 측정 목록에 액세스하고 참조 목록 및 현재 무결성 목록을 만들고 서명 된 TPM 따옴표를 사용하여 측정 목록을 고정 (인증)한다. 이러한 보고서는 응용 프로그램과 공급 업체 간의 상호 운용성을 위해 PTS 표준 형식으로 작성된다.

 

참고

LSM(Linux Security Module) -1-

LSM(Linux Security Module) -1-

CONFIG_SECURITY 커널 옵션을 사용하여 리눅스 시큐리티 기능을 활용할 수 있다.

필요한 시큐리티 모듈을 로드하여 사용한다.

  • rpi2: 디폴트 설정으로 사용하지 않게 설정되어 있다.
  • arm64: 디폴트 설정으로 CONFIG_SECURITY 커널 옵션을 사용하며, 추가적으로 관련 모듈들을 로드하여 사용할 수 있다.

 

참고로 리눅스 개발자들은 LSM의 접근 기능 제어가 다른 기능으로의 남용이 될 수 있는 가능성이 있으므로 좋아하지 않는다.

  • windows OS등과 같이 각 접근 제어용 기능 함수들에 대한 hook을 사용하는데 원래 기능적 목적과 달리 악의적인 모듈(rootkit)을 설치할 수도 있다.

 

Security Module

 

시큐리티 모듈 등록

register_security()

security/security.c

/**
 * register_security - registers a security framework with the kernel
 * @ops: a pointer to the struct security_options that is to be registered
 *
 * This function allows a security module to register itself with the
 * kernel security subsystem.  Some rudimentary checking is done on the @ops
 * value passed to this function. You'll need to check first if your LSM
 * is allowed to register its @ops by calling security_module_enable(@ops).
 *
 * If there is already a security module registered with the kernel,
 * an error will be returned.  Otherwise %0 is returned on success.
 */
int __init register_security(struct security_operations *ops)
{
        if (verify(ops)) {
                printk(KERN_DEBUG "%s could not verify "
                       "security_operations structure.\n", __func__);
                return -EINVAL;
        }

        if (security_ops != &default_security_ops)
                return -EAGAIN;

        security_ops = ops;

        return 0;
}

 

커널파라메터로 시큐리티모듈 이름 등록

choose_lsm()

security/security.c

/* Save user chosen LSM */
static int __init choose_lsm(char *str)
{
        strncpy(chosen_lsm, str, SECURITY_NAME_MAX);
        return 1;
}
__setup("security=", choose_lsm);

“security=” 커널 파라메터를 사용하여 리눅스 시큐리티 모듈의 이름을 설정한다.

 

security_module_enable()

security/security.

/**
 * security_module_enable - Load given security module on boot ?
 * @ops: a pointer to the struct security_operations that is to be checked.
 *
 * Each LSM must pass this method before registering its own operations
 * to avoid security registration races. This method may also be used
 * to check if your LSM is currently loaded during kernel initialization.
 *
 * Return true if:
 *      -The passed LSM is the one chosen by user at boot time,
 *      -or the passed LSM is configured as the default and the user did not
 *       choose an alternate LSM at boot time.
 * Otherwise, return false.
 */
int __init security_module_enable(struct security_operations *ops)
{
        return !strcmp(ops->name, chosen_lsm);
}

부트 타임에 주어진 “security=” 커널 파라메터로 설정한 시큐리티 모듈의 이름과 비교하여 모듈이 활성화(로드)되었는지 확인한다.

 

Common Capability – 가상 주소 매핑 허용 체크

security_mmap_addr()

include/linux/security.h

static inline int security_mmap_addr(unsigned long addr)
{
        return cap_mmap_addr(addr);
}

가상 주소 요청 시 금지 여부를 반환한다. 0=성공, 그 외=금지

 

cap_mmap_addr()

security/commoncap.c

/*
 * cap_mmap_addr - check if able to map given addr
 * @addr: address attempting to be mapped
 *
 * If the process is attempting to map memory below dac_mmap_min_addr they need
 * CAP_SYS_RAWIO.  The other parameters to this function are unused by the
 * capability security module.  Returns 0 if this mapping should be allowed
 * -EPERM if not.
 */
int cap_mmap_addr(unsigned long addr)
{
        int ret = 0;

        if (addr < dac_mmap_min_addr) {
                ret = cap_capable(current_cred(), &init_user_ns, CAP_SYS_RAWIO,
                                  SECURITY_CAP_AUDIT);
                /* set PF_SUPERPRIV if it turns out we allow the low mmap */
                if (ret == 0)
                        current->flags |= PF_SUPERPRIV;
        }
        return ret;
}

가상 주소 요청 시 dac_mmap_min_addr 이하의 경우 필요 보안 권한 이 설정되지 않은 경우 에러를 반환한다. 0=성공

  • arm: dac_mmap_min_addr에 기본값으로 CONFIG_DEFAULT_MMAP_MIN_ADDR(arm=0x1000, x86=0x10000)이다.
  • “/proc/sys/vm/mmap_min_addr” 에서 설정할 수 있다.

 

Common Capability – 새로운 가상 매핑의 할당 허용 제한

security_vm_enough_memory_mm()

include/linux/security.h

static inline int security_vm_enough_memory_mm(struct mm_struct *mm, long pages)
{
        return cap_vm_enough_memory(mm, pages);
}

새로운 요청 페이지 수 만큼의 가상 매핑의 할당을 허가할지 여부를 반환한다.

 

cap_vm_enough_memory()

security/commoncap.c

/**             
 * cap_vm_enough_memory - Determine whether a new virtual mapping is permitted
 * @mm: The VM space in which the new mapping is to be made
 * @pages: The size of the mapping
 *
 * Determine whether the allocation of a new virtual mapping by the current
 * task is permitted, returning 0 if permission is granted, -ve if not.
 */                     
int cap_vm_enough_memory(struct mm_struct *mm, long pages)
{               
        int cap_sys_admin = 0; 
        
        if (cap_capable(current_cred(), &init_user_ns, CAP_SYS_ADMIN,
                        SECURITY_CAP_NOAUDIT) == 0)
                cap_sys_admin = 1;
        return __vm_enough_memory(mm, pages, cap_sys_admin);
}

요청 메모리 디스크립터에 대해 새로운 요청 페이지 수 만큼의 가상 매핑의 할당을 허가할지 여부를 반환한다.

  • 현재 태스크가 admin 자격이 있는 경우에 인수 cap_sys_admin=1로 __vm_enough_memory() 함수가 호출된다.

 

__vm_enough_memory()

mm/mmap.c

/*
 * Check that a process has enough memory to allocate a new virtual
 * mapping. 0 means there is enough memory for the allocation to
 * succeed and -ENOMEM implies there is not.
 *
 * We currently support three overcommit policies, which are set via the
 * vm.overcommit_memory sysctl.  See Documentation/vm/overcommit-accounting
 *
 * Strict overcommit modes added 2002 Feb 26 by Alan Cox.
 * Additional code 2002 Jul 20 by Robert Love.
 *
 * cap_sys_admin is 1 if the process has admin privileges, 0 otherwise.
 *
 * Note this is a helper function intended to be used by LSMs which
 * wish to use this logic.
 */
int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin)
{
        long free, allowed, reserve;

        VM_WARN_ONCE(percpu_counter_read(&vm_committed_as) <
                        -(s64)vm_committed_as_batch * num_online_cpus(),
                        "memory commitment underflow");

        vm_acct_memory(pages);

        /*
         * Sometimes we want to use more memory than we have
         */
        if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
                return 0;

        if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
                free = global_page_state(NR_FREE_PAGES);
                free += global_page_state(NR_FILE_PAGES);

                /*
                 * shmem pages shouldn't be counted as free in this
                 * case, they can't be purged, only swapped out, and
                 * that won't affect the overall amount of available
                 * memory in the system.
                 */
                free -= global_page_state(NR_SHMEM);

                free += get_nr_swap_pages();

                /*
                 * Any slabs which are created with the
                 * SLAB_RECLAIM_ACCOUNT flag claim to have contents
                 * which are reclaimable, under pressure.  The dentry
                 * cache and most inode caches should fall into this
                 */
                free += global_page_state(NR_SLAB_RECLAIMABLE);

                /*
                 * Leave reserved pages. The pages are not for anonymous pages.
                 */
                if (free <= totalreserve_pages)
                        goto error;
                else
                        free -= totalreserve_pages;

                /*
                 * Reserve some for root
                 */
                if (!cap_sys_admin)
                        free -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

                if (free > pages)
                        return 0;

                goto error;
        }

        allowed = vm_commit_limit();
        /*
         * Reserve some for root
         */
        if (!cap_sys_admin)
                allowed -= sysctl_admin_reserve_kbytes >> (PAGE_SHIFT - 10);

        /*
         * Don't let a single process grow so big a user can't recover
         */
        if (mm) {
                reserve = sysctl_user_reserve_kbytes >> (PAGE_SHIFT - 10);
                allowed -= min_t(long, mm->total_vm / 32, reserve);
        }

        if (percpu_counter_read_positive(&vm_committed_as) < allowed)
                return 0;
error:
        vm_unacct_memory(pages);

        return -ENOMEM;
}

현재 태스크에서 새로운 요청 페이지 수 만큼의 가상 매핑의 할당을 허가할지 여부를 반환한다

  • 코드 라인 25에서 per-cpu 전역 변수 vm_committed_as 카운터에 pages 수 만큼 추가한다.
    • commit된 페이지 수를 accounting한다.
  • 코드 라인 30~31에서 over commit이 항상 허용되는 경우 성공적으로 0을 반환한다.
    • “/proc/sys/vm/overcommit_memory”
      • 0: guess (free 페이지 수 + file 페이지 수 + 스왑 페이지 수 + slab 회수 가능 페이지 수 – 전체 리저브 페이지 수 – admin 리저브 페이지 수 만큼 가능)
      • 1: 항상 over commit이 가능하다.
      • 2: over commit 할 수 없고 아래 두 파라메터로 결정한다
  • 코드 라인 33에서 over commit 가능량을 sysctl 값에 의존한다.
  • 코드 라인 34~53에서 free 페이지 + file 페이지 – 공유메모리 페이지 + 스왑 페이지 수 + slab 회수 페이지 수를 free에 대입한다.
  • 코드 라인  58~59에서 free가 전체 reserve 페이지 수보다 작은 경우 error 레이블로 이동한다.
  • 코드 라인 60~61에서 free에서 전체 reserve 페이지 수를 뺀다.
  •  코드 라인 66~67 현재 태스크가 user 권한인 경우 free에서 admin 리저브 페이지 수 만큼을 뺀다.
    •  “/ proc/sys/vm/admin_reserve_kbytes”
      • 디폴트로 8MB
  • 코드 라인 69~72에서 free가 요청 페이지 수보다 큰 경우 성공리에 0을 반환하고 그렇지 않은 경우 error 레이블로 이동한다.
  • 코드 라인 75에서 over commit 제한치 이상을 허용하지 않는 경우이다. 먼저 over commit 제한치를 알아온다.
  • 코드 라인 79~80에서 현재 태스크가 user 권한인 경우 free에서 admin 리저브 페이지 수 만큼을 뺀다.
  • 코드 라인 85~88에서 단일 프로세서가 너무 많은 할당을 할 수 없도록 허용치를 약간 제한한다.
    • 메모리 디스크립터 mm이 지정된 경우 태스크의 전체 vm 페이지의 1/32와 유저 reserve 페이지 중 작은 값을 allowed에서 감소시킨다.
    • “/ proc/sys/vm/user_reserve_kbytes”
      • 디폴트로 128MB
  • 코드 라인 90~91에서 vm commit된 페이지 수가 allowed보다 작은 경우 성공리에 0을 반환한다.
  • 코드 라인 92~95에서 error 처리를 하는 레이블로 미리 commit 시킨 페이지 수를 감소시킨다.

 

vm_commit_limit()

mm/util.c

/*
 * Committed memory limit enforced when OVERCOMMIT_NEVER policy is used
 */
unsigned long vm_commit_limit(void)
{
        unsigned long allowed;

        if (sysctl_overcommit_kbytes)
                allowed = sysctl_overcommit_kbytes >> (PAGE_SHIFT - 10);
        else
                allowed = ((totalram_pages - hugetlb_total_pages())
                           * sysctl_overcommit_ratio / 100);
        allowed += total_swap_pages;

        return allowed;
}

over commit 제한치 이상을 허용하지 않는 정책을 사용하는 경우를 위해 over commit 제한치를 산출한다.

  • 코드 라인 8~9에서 “/proc/sys/vm/overcommit_kbytes”가 0이 아닌 경우 지정된 값 x kbytes를 allowed에 대입한다.
  • 코드 라인 10~12에서 그렇지 않은 경우 전체 메모리 페이지 – 전체 hugetlb 페이지 수를 over commit 백분율 비율로 allowed에 대입한다.
    • “/proc/sys/vm/overcommit_ratio”
      • 디폴트로 50%
  • 코드 라인 13~15에서 계산된 허용 페이지 수에 전체 스왑 페이지 수를 더한다.

참고

  • LSM(Linux Security Module) -1- | 문c – 현재글
  • LSM -2- (AppArmor) | 문c – not yet
  • LSM -3- (SELinux) | 문c – not yet
  • LSM -4- (SMACK) | 문c – not yet
  • LSM -5- (TOMOYO) | 문c – not yet
  • LSM -6- (Yama) | 문c – not yet
  • LIM(Linux Integrity Module) -1- | 문c

 

User virtual maps (brk)

<kernel v5.0>

유저 프로세스의 힙 또는 스택 메모리 증/감 요청

Heap Manager

user space에서 malloc() 함수를 사용 시 glibc등의 라이브러리에 포함된 힙 메모리 관리자가 힙 pool을 관리한다. 만일 힙 메모리가 더 필요한 경우 커널에게 Posix system call을 통해 유저 anon 메모리 요청을 하게되는데 이 때 커널에서 호출되는 함수는 brk() 또는 mmap()이다.

  • M_MMAP_THRESHOLD (128K)
    • mallopt() 함수를 사용하여 스레졸드 기준치를 변경할 수 있다.
    • 스레졸드 이하의 힙을 확장하려할 때 brk() 함수가 호출되며 이는 mmap()의 간단한 버전으로 anonymouse 타입의 메모리만 취급한다.
    • 스레졸드 이상의 힙을 확장하는 경우에는 mmap() 함수가 호출된다.

 

Custom Heap Manager

대부분의 경우 힙 관리를 위해 C에서는 GNU의 glibc 라이브러리, 그리고 assembly에서는 FASMLIB 라이브러리를 사용하는 것이 쉽고 편하다. 그런데 이러한 라이브러리를 사용하지 않고 특별히 custom 힙 관리자를 사용해야 하는 경우도 있다. 당연히 개발자가 직접 복잡한 스레드들을 위해 특별한 힙 관리자를 만들기 위해 brk() 또는 map() 시스템 콜 함수를 호출하는 custom 힙 메모리 관리자를 작성할 수 있다.

 

sys_brk()

mm/mmap.c -1/2-

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
        unsigned long retval;
        unsigned long newbrk, oldbrk, origbrk;
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *next;
        unsigned long min_brk;
        bool populate;
        bool downgraded = false;
        LIST_HEAD(uf);

        if (down_write_killable(&mm->mmap_sem))
                return -EINTR;

        origbrk = mm->brk;

#ifdef CONFIG_COMPAT_BRK
        /*
         * CONFIG_COMPAT_BRK can still be overridden by setting
         * randomize_va_space to 2, which will still cause mm->start_brk
         * to be arbitrarily shifted
         */
        if (current->brk_randomized)
                min_brk = mm->start_brk;
        else
                min_brk = mm->end_data;
#else
        min_brk = mm->start_brk;
#endif
        if (brk < min_brk)
                goto out;

        /*
         * Check against rlimit here. If this check is done later after the test
         * of oldbrk with newbrk then it can escape the test and let the data
         * segment grow beyond its set limit the in case where the limit is
         * not page aligned -Ram Gupta
         */
        if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
                              mm->end_data, mm->start_data))
                goto out;

        newbrk = PAGE_ALIGN(brk);
        oldbrk = PAGE_ALIGN(mm->brk);
        if (oldbrk == newbrk) {
                mm->brk = brk;
                goto success;
        }

        /*
         * Always allow shrinking brk.
         * __do_munmap() may downgrade mmap_sem to read.
         */
        if (brk <= mm->brk) {
                int ret;

                /*
                 * mm->brk must to be protected by write mmap_sem so update it
                 * before downgrading mmap_sem. When __do_munmap() fails,
                 * mm->brk will be restored from origbrk.
                 */
                mm->brk = brk;
                ret = __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true);
                if (ret < 0) {
                        mm->brk = origbrk;
                        goto out;
                } else if (ret == 1) {
                        downgraded = true;
                }
                goto success;
        }

힙(데이터) 영역을 확장하거나 축소한다. 확장 시 anon 유저 페이지로 할당하고, 축소 시 해당 영역을 언매핑 한다.

  • 코드 라인 23~28에서 보안 목적으로 user space에 배치되는 힙 위치를 랜덤하게 사용하는데 레거시 libc5 등 호환  목적으로 이 커널 옵션을 사용하여 힙 시작 위치 랜덤 기능을 사용하지 않게 한다.
    • “echo 2 > /proc/sys/kernel/randomize_va_space”를 수행하는 경우 힙 시작 위치 랜덤 기능을 사용하게 한다.
    • “norandmaps” 커널 파라메터를 사용하는 경우에도 힙 시작 위치 랜덤 기능을 사용하지 않게 할 수 있다.
  • 코드 라인 30~31에서 태스크에 설정된 힙 시작 주소 보다 낮은 힙 주소를 요청하는 경우 할당을 포기하고 out 레이블로 이동한다.
  • 코드 라인 39~41에서 현재 태스크가 사용하는 데이터 크기가 현재 태스크에 설정된 data 영역 한계 용량을 초과하는 경우 할당을 포기하고 out 레이블로 이동한다.
    • RLIMIT_DATA: 데이터(힙 사용량 + 데이터 섹션) 사이즈 limit
  • 코드 라인 43~48에서 요청한 힙 주소의 페이지 단위 변화가 없는 경우 success 레이블로 이동한다.
  • 코드 라인 54~71에서 요청한 힙 주소가 태스크에 설정한 기존 힙 주소 이하인 경우 힙을 그 줄어든 차이 언매핑하여 축소(shrink) 한다. 축소가 성공하면 success 레이블로 이동하고 그렇지 않은 경우 out 레이블로 이동한다.

 

mm/mmap.c -2/2-

        /* Check against existing mmap mappings. */
        next = find_vma(mm, oldbrk);
        if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
                goto out;

        /* Ok, looks good - let it rip. */
        if (do_brk_flags(oldbrk, newbrk-oldbrk, 0, &uf) < 0)
                goto out;
        mm->brk = brk;

success:
        populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
        if (downgraded)
                up_read(&mm->mmap_sem);
        else
                up_write(&mm->mmap_sem);
        userfaultfd_unmap_complete(mm, &uf);
        if (populate)
                mm_populate(oldbrk, newbrk - oldbrk);
        return brk;

out:
        retval = origbrk;
        up_write(&mm->mmap_sem);
        return retval;
}
  • 코드 라인 2~4에서 늘어날 힙 공간이 기존에 이미 매핑되어 있는 경우 할당을 포기하고 out 레이블로 이동한다.
  • 코드 라인 7~8에서 기존 vm을 확장하여 반환하거나 새 vm을 구성하여 가져온다. 실패하는 경우 out 레이블로 이동한다.
  • 코드 라인 11~19에서 success: 레이블이다. 늘어난 힙이 있으면서 mlocked 영역의 vma인 경우 해당 영역에 대한 유저 페이지를 모두 할당하여 매핑한다.
  • 코드 라인 20에서 새 힙의 끝 주소를 반환한다.
  • 코드 라인 22~25에서 out: 레이블이다. 기존 힙 끝 주소를 반환한다.

 

다음 그림은 do_brk() 함수가 호출될 때 힙이 확장되거나 줄어드는 모습을 보여준다.

 

check_data_rlimit()

include/linux/mm.h

static inline int check_data_rlimit(unsigned long rlim, 
                                    unsigned long new,
                                    unsigned long start,
                                    unsigned long end_data,
                                    unsigned long start_data)
{
        if (rlim < RLIM_INFINITY) {
                if (((new - start) + (end_data - start_data)) > rlim)
                        return -ENOSPC;
        }

        return 0;
}

데이터 사용 크기가 rlim을 초과하는 경우 -ENOSPC 에러 값을 반환한다.

  • 데이터=힙 사용량 + 데이터 섹션

 

do_brk_flags()

mm/mmap.c

/*
 *  this is really a simplified "do_mmap".  it only handles
 *  anonymous maps.  eventually we may be able to do some
 *  brk-specific accounting here.
 */
static int do_brk_flags(unsigned long addr, unsigned long len, unsigned long flags, struct list_headd
 *uf)
{
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma, *prev;
        struct rb_node **rb_link, *rb_parent;
        pgoff_t pgoff = addr >> PAGE_SHIFT;
        int error;

        /* Until we need other flags, refuse anything except VM_EXEC. */
        if ((flags & (~VM_EXEC)) != 0)
                return -EINVAL;
        flags |= VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;

        error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);
        if (offset_in_page(error))
                return error;

        error = mlock_future_check(mm, mm->def_flags, len);
        if (error)
                return error;

        /*
         * Clear old maps.  this also does some error checking for us
         */
        while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
                              &rb_parent)) {
                if (do_munmap(mm, addr, len, uf))
                        return -ENOMEM;
        }

        /* Check against address space limits *after* clearing old maps... */
        if (!may_expand_vm(mm, flags, len >> PAGE_SHIFT))
                return -ENOMEM;

        if (mm->map_count > sysctl_max_map_count)
                return -ENOMEM;

        if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
                return -ENOMEM;

        /* Can we just expand an old private anonymous mapping? */
        vma = vma_merge(mm, prev, addr, addr + len, flags,
                        NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
        if (vma)
                goto out;

        /*
         * create a vma struct for an anonymous mapping
         */
        vma = vm_area_alloc(mm);
        if (!vma) {
                vm_unacct_memory(len >> PAGE_SHIFT);
                return -ENOMEM;
        }

        vma_set_anonymous(vma);
        vma->vm_start = addr;
        vma->vm_end = addr + len;
        vma->vm_pgoff = pgoff;
        vma->vm_flags = flags;
        vma->vm_page_prot = vm_get_page_prot(flags);
        vma_link(mm, vma, prev, rb_link, rb_parent);
out:
        perf_event_mmap(vma);
        mm->total_vm += len >> PAGE_SHIFT;
        mm->data_vm += len >> PAGE_SHIFT;
        if (flags & VM_LOCKED)
                mm->locked_vm += (len >> PAGE_SHIFT);
        vma->vm_flags |= VM_SOFTDIRTY;
        return 0;
}

현재 태스크 유저 영역에서 요청한 가상 주소와 길이만큼의 영역을 고정 매핑한다. 성공 시 0을 반환한다.

  • 코드 라인 7에서 가상 주소에 대한 페이지 번호를 pgoff에 담는다.
    • 캐시 aliasing을 사용하는 시스템에서 시작 주소가 적절한지 비교하기 위해 사용된다.
  • 코드 라인 11~12에서 VM_EXEC를 제외한 모든 플래그의 사용을 허용하지 않는다.
  • 코드 라인 13에서 코드 라인 19에서 VM_DATA_DEFAULT_FLAGS와 VM_ACCOUNT 및 현재 태스크의 def_flags를 담는다.
    • VM_DATA_DEFAULT_FLAGS:
      • VM_EXEC(옵션) | VM_READ | VM_WRITE | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC
  • 코드 라인 15~17에서 현재 태스크 유저 영역의 요청 가상 주소부터 길이 len만큼의 빈 공간이 있어 고정  매핑시킬 수 있는지 여부를 알아온다. 만일 매핑할 공간이 없으면 error를 반환한다.
  • 코드 라인 19~21에서 현재 태스크에서 VM_LOCKED 플래그를 사용한 페이지들의 수가 최대 locked 페이지 수 제한을 초과하는 경우 -EAGAIN 에러를 반환한다.
  • 코드 라인 26~30에서 vma들에서 요청 가상 주소 영역과 겹치는 경우 언맵을 수행한다.
  • 코드 라인 33~34에서 현재 태스크의 전체 vm 페이지 수가 주소 공간 제한(RLIMIT_AS)을 초과하는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 36~37에서 현재 태스크의 vma 수가 최대 매핑 카운트 수를 초과하는 경우 -ENOMEM을 반환한다.
    • sysctl_max_map_count
      • 디폴트로 65530이며 이 값은 “/proc/sys/vm/max_map_count” 를 통해 변경할 수 있다.
  • 코드 라인 39~40에서 LSM(Linux Security Module)을 통해 새로운 가상 매핑의 할당이 충분한지 여부를 체크한다.
  • 코드 라인 43~46에서 기존 vma가 private anonymous 매핑을 사용하는 경우 merge하여 확장할 수 있는지 확인하고 가능하면 out 레이블로 이동한다.
  • 코드 라인 51~55에서 vma 구조체용 메모리를 할당하고 실패시 이미 증가시킨 vm commit  페이지 수를 다시 원상복귀 시키고 -ENOMEM 에러를 반환한다.
  • 코드 라인 57~63에서 vma 정보를 구성하고 추가한다.
    • vm_page_prot에 들어가는 매핑 속성은 메모리 디스크립터에 특별히 추가된 속성(mm->def_flags)이 없어도 기본적으로 __PAGE_COPY이다.
  • 코드 라인 64~67에서 out 레이블의 시작 위치이며 메모리 디스크립터에 전체 사용 vm 페이지 수를 갱신한다.
  • 코드 라인 68~69에서 VM_LOCKED 플래그를 사요하는 경우 메모리 디스크립터의 locked_vm 페이지 카운터를 갱신한다.
  • 코드 라인 70에서 vma의 플래그에 VM_SOFTDIRTY를 추가한다.
  • 코드 라인 71에서 성공 0을 반환한다.

 

다음 그림은 brk() 요청에 대하여 기존 vma를 사용하여 확장하는 모습을 보여준다.

 


vm 플래그 -> 페이지 매핑 속성

vm_get_page_prot()

mm/mmap.c

pgprot_t vm_get_page_prot(unsigned long vm_flags)
{
        return __pgprot(pgprot_val(protection_map[vm_flags &
                                (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
                        pgprot_val(arch_vm_get_page_prot(vm_flags)));
}
EXPORT_SYMBOL(vm_get_page_prot);

vm 플래그들 중 read, write, exec, shared 플래그 값의 사용 우무로 필요한 페이지 매핑 속성 값들을 알아온다.

 

전역 protection_map[] 배열

mm/mmap.c

/* description of effects of mapping type and prot in current implementation.
 * this is due to the limited x86 page protection hardware.  The expected
 * behavior is in parens:
 *
 * map_type     prot
 *              PROT_NONE       PROT_READ       PROT_WRITE      PROT_EXEC
 * MAP_SHARED   r: (no) no      r: (yes) yes    r: (no) yes     r: (no) yes
 *              w: (no) no      w: (no) no      w: (yes) yes    w: (no) no
 *              x: (no) no      x: (no) yes     x: (no) yes     x: (yes) yes
 *
 * MAP_PRIVATE  r: (no) no      r: (yes) yes    r: (no) yes     r: (no) yes
 *              w: (no) no      w: (no) no      w: (copy) copy  w: (no) no
 *              x: (no) no      x: (no) yes     x: (no) yes     x: (yes) yes
 *
 */
pgprot_t protection_map[16] __ro_after_init = {
        __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
        __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
};

4개의 플래그 비트로 16개의 배열이 작성된다.

 

16개의 페이지 매핑 속성 – ARM32

arch/arm/include/asm/pgtable.h

/*
 * The table below defines the page protection levels that we insert into our
 * Linux page table version.  These get translated into the best that the
 * architecture can perform.  Note that on most ARM hardware:
 *  1) We cannot do execute protection
 *  2) If we could do execute protection, then read is implied
 *  3) write implies read permissions
 */
#define __P000  __PAGE_NONE
#define __P001  __PAGE_READONLY
#define __P010  __PAGE_COPY
#define __P011  __PAGE_COPY
#define __P100  __PAGE_READONLY_EXEC
#define __P101  __PAGE_READONLY_EXEC
#define __P110  __PAGE_COPY_EXEC
#define __P111  __PAGE_COPY_EXEC

#define __S000  __PAGE_NONE
#define __S001  __PAGE_READONLY
#define __S010  __PAGE_SHARED
#define __S011  __PAGE_SHARED
#define __S100  __PAGE_READONLY_EXEC
#define __S101  __PAGE_READONLY_EXEC
#define __S110  __PAGE_SHARED_EXEC
#define __S111  __PAGE_SHARED_EXEC

read, write, exec, shared 플래그 비트로 조합한 16개의 매크로 상수 값

arch/arm/include/asm/pgtable.h

#define __PAGE_NONE             __pgprot(_L_PTE_DEFAULT | L_PTE_RDONLY | L_PTE_XN | L_PTE_NONE)
#define __PAGE_SHARED           __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_XN)
#define __PAGE_SHARED_EXEC      __pgprot(_L_PTE_DEFAULT | L_PTE_USER)
#define __PAGE_COPY             __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)
#define __PAGE_COPY_EXEC        __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)
#define __PAGE_READONLY         __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)
#define __PAGE_READONLY_EXEC    __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)
  • __PAGE_NONE
    • NUMA 시스템에서 해당 페이지를 읽을 때 accesss 권한 실패로 인해 abort exception이 발생되어 fault된 후 해당 페이지를 사용하는 태스크의 migration을 고려하는 Automatic NUMA balancing을 위해 사용된다.
  • __PAGE_COPY
    • L_PTE_PRESENT: 매핑됨
    • L_PTE_YOUNG: 페이지에 접근 가능
    • L_PTE_USER: 유저 허용
    • L_PTE_RDONLY: 읽기 전용
    • L_PTE_XN: 실행 금지(Excute Never)

 

16개의 페이지 매핑 속성 – ARM64

arch/arm64/include/asm/pgtable-prot.h

#define __P000  PAGE_NONE
#define __P001  PAGE_READONLY
#define __P010  PAGE_READONLY
#define __P011  PAGE_READONLY
#define __P100  PAGE_EXECONLY
#define __P101  PAGE_READONLY_EXEC
#define __P110  PAGE_READONLY_EXEC
#define __P111  PAGE_READONLY_EXEC

#define __S000  PAGE_NONE
#define __S001  PAGE_READONLY
#define __S010  PAGE_SHARED
#define __S011  PAGE_SHARED
#define __S100  PAGE_EXECONLY
#define __S101  PAGE_READONLY_EXEC
#define __S110  PAGE_SHARED_EXEC
#define __S111  PAGE_SHARED_EXEC

 

arch/arm64/include/asm/pgtable-prot.h

#define PAGE_NONE               __pgprot(((_PAGE_DEFAULT) & ~PTE_VALID) | PTE_PROT_NONE | PTE_RDONLYY
 | PTE_NG | PTE_PXN | PTE_UXN)
#define PAGE_SHARED             __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_UXN | PTEE
_WRITE)
#define PAGE_SHARED_EXEC        __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_WRITE)
#define PAGE_READONLY           __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN |
PTE_UXN)
#define PAGE_READONLY_EXEC      __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN)
#define PAGE_EXECONLY           __pgprot(_PAGE_DEFAULT | PTE_RDONLY | PTE_NG | PTE_PXN)
  • _PAGE_DEFAULT
    • PTE_ATTRINDX(MT_NORMAL) | PTE_TYPE_PAGE | PTE_AF | PTE_SHARED

 


매핑되지 않은 주소 영역 찾기

get_unmapped_area()

mm/mmap.c

unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags)
{
        unsigned long (*get_area)(struct file *, unsigned long,
                                  unsigned long, unsigned long, unsigned long);

        unsigned long error = arch_mmap_check(addr, len, flags);
        if (error)
                return error;

        /* Careful about overflows.. */
        if (len > TASK_SIZE)
                return -ENOMEM;

        get_area = current->mm->get_unmapped_area;
        if (file) {
                if (file->f_op->get_unmapped_area)
                        get_area = file->f_op->get_unmapped_area;
        } else if (flags & MAP_SHARED) {
                /*
                 * mmap_region() will call shmem_zero_setup() to create a file,
                 * so use shmem's get_unmapped_area in case it can be huge.
                 * do_mmap_pgoff() will clear pgoff, so match alignment.
                 */
                pgoff = 0;
                get_area = shmem_get_unmapped_area;
        }

        addr = get_area(file, addr, len, pgoff, flags);
        if (IS_ERR_VALUE(addr))
                return addr;

        if (addr > TASK_SIZE - len)
                return -ENOMEM;
        if (offset_in_page(addr))
                return -EINVAL;

        error = security_mmap_addr(addr);
        return error ? error : addr;
}
EXPORT_SYMBOL(get_unmapped_area);

현재 태스크 유저 영역의 mmap 매핑 공간에서 검색하여 요청 길이가 들어갈 수 있는 매핑되지 않은 빈 영역의 가상 시작 주소를 알아온다.

  • 코드 라인 8~10에서 요청한 유저 공간이 아키텍처가 지원하지 않는 매핑 공간인 경우 에러를 반환한다.
  • 코드 라인 13~14에서 요청 할당 길이가 유저 공간 크기보다 큰 경우 -ENOMEM 에러를 반환한다.
    • TASK_SIZE: 유저 공간의 크기
      • 예) rpi2: 2G – 16M (arm에서는 커널 공간과 모듈(16M) 공간을 제외한 크기)
  • 코드 라인 16~32에서 언맵된 영역을 알아온다.
    • file 매핑인 경우 file 디스크립터로부터 언맵된 영역을 알아오는 핸들러 함수를 사용한다.
      • 참고로 do_brk()는 file 매핑을 사용하지 않고 anon 매핑을 위해 file 인수에 null을 대입하여 호출한다.
    • shared 영역에 대한 핸들러 함수는 shmem_get_unmapped_area() 함수를 사용한다.
  • 코드 라인 34~35에서 알아온 가상 주소를 유저 공간에 배치할 수 없는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 36~37에서 알아온 가상 주소가 0페이지인 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 39~30에서 보안을 위해 사용되는 selinux가 사용될 때, 해당 가상 주소가 허가된 주소인 경우 그 주소를 반환하고 그렇지 않은 경우 에러를 반환한다.

 

arch_mmap_check()

arch/arm/include/uapi/asm/mman.h

#define arch_mmap_check(addr, len, flags) \
        (((flags) & MAP_FIXED && (addr) < FIRST_USER_ADDRESS) ? -EINVAL : 0)

요청한 가상주소로 고정 매핑을 요구하는 경우 그 가상 주소가 시작 유저 공간 주소 이하인 경우 -EINVAL을 반환한다.

  • FIRST_USER_ADDRESS: (PAGE_SIZE * 2)
    • arm에서 로우 벡터를 사용하는 경우 하위 2개 페이지는 사용자가 사용할 수 없는 공간이다.
  • ARM64 및 x86의 경우 항상 성공 0을 반환한다.

 

arch_get_unmapped_area() – ARM32

arch/arm/mm/mmap.c

/*
 * We need to ensure that shared mappings are correctly aligned to
 * avoid aliasing issues with VIPT caches.  We need to ensure that
 * a specific page of an object is always mapped at a multiple of
 * SHMLBA bytes.
 *
 * We unconditionally provide this function for all cases, however
 * in the VIVT case, we optimise out the alignment rules.
 */
unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,
                unsigned long len, unsigned long pgoff, unsigned long flags)
{
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma;
        int do_align = 0;
        int aliasing = cache_is_vipt_aliasing();
        struct vm_unmapped_area_info info;

        /*
         * We only need to do colour alignment if either the I or D
         * caches alias.
         */
        if (aliasing)
                do_align = filp || (flags & MAP_SHARED);

        /*
         * We enforce the MAP_FIXED case.
         */
        if (flags & MAP_FIXED) {
                if (aliasing && flags & MAP_SHARED &&
                    (addr - (pgoff << PAGE_SHIFT)) & (SHMLBA - 1))
                        return -EINVAL;
                return addr;
        }

        if (len > TASK_SIZE)
                return -ENOMEM;

        if (addr) {
                if (do_align)
                        addr = COLOUR_ALIGN(addr, pgoff);
                else
                        addr = PAGE_ALIGN(addr);

                vma = find_vma(mm, addr);
                if (TASK_SIZE - len >= addr &&
                    (!vma || addr + len <= vma->vm_start))
                        return addr;
        }

        info.flags = 0;
        info.length = len;
        info.low_limit = mm->mmap_base;
        info.high_limit = TASK_SIZE;
        info.align_mask = do_align ? (PAGE_MASK & (SHMLBA - 1)) : 0;
        info.align_offset = pgoff << PAGE_SHIFT;
        return vm_unmapped_area(&info);
}

arm 아키텍처용으로 구현된 함수로 현재 태스크 유저 영역의 mmap 매핑 공간에서 아래에서 위로 검색하여 요청 길이가 들어갈 수 있는 매핑되지 않은 빈 영역의 가상 시작 주소를 알아온다.

  • 코드 라인 8에서 현재 아키텍처의 데이터 캐시가 vipt aliasing을 사용하는지 여부를 알아온다.
    • rpi2: aliasing을 사용하지 않는다.
  • 코드 라인 15~16에서 aliasing이 사용되는 경우 do_align 값에 flip(파일 구조체 포인터) 값 + MAP_SHARED(1) 플래그를 대입하여 align이 필요한지 여부를 구한다.
  • 코드 라인 21~26에서 vm 고정 매핑을 요청한 경우 요청한 가상 주소를 그대로 사용해야 하므로 가상 주소를 변경하지 않고 함수를 빠져나간다. 단 aliasing 및 공유맵을 요청받은 경우  pgoff 페이지 만큼을 감소시킨 가상 주소가 aliasing에 필요한 SHMLBA 사이즈 단위로 정렬되지 않는 경우 -EINVAL을 반환한다.
    • SHMLBA: armv7 이전 아키텍처에서 aliasing에 대한 보정때문에 매핑 페이지를 공유해야하는 경우 태스크들 간에 4개 페이지(16K) 이내의 가상 주소 매핑을 사용하지 못하게 한다.
  • 코드 라인 28~29에서 길이가 유저 공간 크기를 초과하는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 31~35에서 가상 주소가 지정된 경우 페이지 단위로 정렬하되 do_align이 설정되어 있으면 가상 주소를 aliasing에 맞게 정렬시킨다.
  • 코드 라인 37에서 현재 태스크에 등록된 vma 정보에서 vma를 찾아온다.
  • 코드 라인 38~40에서 조절된 가상 주소가 유저 공간에 배치 가능하고 vma 영역 이하인 경우 함수를 빠져나간다.
  • 코드 라인 43~49에서 다음과 같이 vm 언맵된 영역 정보를 구성한 후 언맵된 영역을 찾아 반환한다.
    • low_limit에는 매핑 영역 하한 주소를 대입한다.
    • high_limit에는 user 공간의 상한 주소를 대입한다.
    • align_mask는 aliasing이 필요한 arm 아키텍처에 대해 필요한 크기-1 페이지(3 페이지=0x3000)를 대입하여 매핑할 영역 뒷부분을 사용하지 못하게 하는 용도로 배정한다.
    • align_offset는 페이지에서 읽어올 offset 페이지 번호를 대입한다.

 

아래 두 개의 그림은 fixed 매핑 여부에 따라 매핑 공간의 시작 주소와 크기가 캐시 aliasing에 의해 변경될 수 있음을 보여준다.

 

 

COLOUR_ALIGN()

mm/mmap.c

#define COLOUR_ALIGN(addr,pgoff)                \
        ((((addr)+SHMLBA-1)&~(SHMLBA-1)) +      \
         (((pgoff)<<PAGE_SHIFT) & (SHMLBA-1)))

데이터 캐시가 vipt aliasing을 사용하는 경우 페이지 컬러링에 맞추어 요청 가상 주소를 정렬한다.

  • 예) SHMLBA=16K , addr=0x1234_6000, pgoff=7
    • 가상 주소를 16K 단위로 정렬 + pgoff 페이지를 16K 페이지단위로 나눈 나머지
      • 0x1234_8000 + 0x3000 = 0x1234_b000

 

arch_get_unmapped_area() – Generic (x86, ARM64, …)

mm/mmap.c

/* Get an address range which is currently unmapped.
 * For shmat() with addr=0.
 *
 * Ugly calling convention alert:
 * Return value with the low bits set means error value,
 * ie
 *      if (ret & ~PAGE_MASK)
 *              error = ret;
 *
 * This function "knows" that -ENOMEM has the bits set.
 */
#ifndef HAVE_ARCH_UNMAPPED_AREA
unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,
                unsigned long len, unsigned long pgoff, unsigned long flags)
{
        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma, *prev;
        struct vm_unmapped_area_info info;
        const unsigned long mmap_end = arch_get_mmap_end(addr);

        if (len > mmap_end - mmap_min_addr)
                return -ENOMEM;

        if (flags & MAP_FIXED)
                return addr;

        if (addr) {
                addr = PAGE_ALIGN(addr);
                vma = find_vma_prev(mm, addr, &prev);
                if (mmap_end - len >= addr && addr >= mmap_min_addr &&
                    (!vma || addr + len <= vm_start_gap(vma)) &&
                    (!prev || addr >= vm_end_gap(prev)))
                        return addr;
        }

        info.flags = 0;
        info.length = len;
        info.low_limit = mm->mmap_base;
        info.high_limit = mmap_end;
        info.align_mask = 0;
        return vm_unmapped_area(&info);
}
#endif

generic(x86, ARM64 등이 포함됨)으로 구현된 함수로 현재 태스크 유저 영역의 mmap 매핑 공간에서 아래에서 위로 검색하여 요청 길이가 들어갈 수 있는 매핑되지 않은 빈 영역의 가상 시작 주소를 알아온다.

  • 코드 라인 11~12에서 아키텍처가 지원하는 최대 크기를 넘어서면 -ENOMEM 에러를 반환한다.
  • 코드 라인 14~15에서 MAP_FIXED 플래그를 사용한 경우 주소 변환 없이 @addr을 그대로 반환한다.
  • 코드 라인 17~24에서 가상 주소 @addr가 지정된 경우 페이지 단위로 정렬하고, 현재 태스크에 등록된 vma 정보에서 vma를 찾아온다. 만일 가상 주소가 유저 공간에 배치 가능하고 vma 영역 이하인 경우 함수를 빠져나간다.
  • 코드 라인 26~31에서 다음 정보로 언맵된 영역을 찾아 그 가상 주소를 반환한다.
    • low_limit에는 매핑 영역 하한 주소를 대입한다.
    • high_limit에는 user 공간의 상한 주소를 대입한다.

 

arch_get_mmap_base() & arch_get_mmap_end() – ARM64

arch/arm64/include/asm/processor.h

#ifndef CONFIG_ARM64_FORCE_52BIT
#define arch_get_mmap_end(addr) ((addr > DEFAULT_MAP_WINDOW) ? TASK_SIZE :\
                                DEFAULT_MAP_WINDOW)

#define arch_get_mmap_base(addr, base) ((addr > DEFAULT_MAP_WINDOW) ? \
                                        base + TASK_SIZE - DEFAULT_MAP_WINDOW :\
                                        base)
#endif /* CONFIG_ARM64_FORCE_52BIT */

vm_unmapped_area()

include/linux/mm.h

/*                  
 * Search for an unmapped address range.
 *      
 * We are looking for a range that:
 * - does not intersect with any VMA;
 * - is contained within the [low_limit, high_limit) interval;
 * - is at least the desired size.
 * - satisfies (begin_addr & align_mask) == (align_offset & align_mask)
 */
static inline unsigned long 
vm_unmapped_area(struct vm_unmapped_area_info *info)
{
        if (!(info->flags & VM_UNMAPPED_AREA_TOPDOWN))
                return unmapped_area(info);
        else
                return unmapped_area_topdown(info);
}

언맵된 영역의 가상 주소를 알아온다.

  • VM_UNMAPPED_AREA_TOPDOWN 플래그 사용 여부에 따라 검색 방향이 결정된다.

 

아래에서 위로 주소 영역 찾기

unmapped_area()

mm/mmap.c -1/2-

unsigned long unmapped_area(struct vm_unmapped_area_info *info)
{
        /*
         * We implement the search by looking for an rbtree node that
         * immediately follows a suitable gap. That is,
         * - gap_start = vma->vm_prev->vm_end <= info->high_limit - length;
         * - gap_end   = vma->vm_start        >= info->low_limit  + length;
         * - gap_end - gap_start >= length
         */

        struct mm_struct *mm = current->mm;
        struct vm_area_struct *vma;
        unsigned long length, low_limit, high_limit, gap_start, gap_end;

        /* Adjust search length to account for worst case alignment overhead */
        length = info->length + info->align_mask;
        if (length < info->length)
                return -ENOMEM;

        /* Adjust search limits by the desired length */
        if (info->high_limit < length)
                return -ENOMEM;
        high_limit = info->high_limit - length;

        if (info->low_limit > high_limit)
                return -ENOMEM;
        low_limit = info->low_limit + length;

        /* Check if rbtree root looks promising */
        if (RB_EMPTY_ROOT(&mm->mm_rb))
                goto check_highest;
        vma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);
        if (vma->rb_subtree_gap < length)
                goto check_highest;

        while (true) {
                /* Visit left subtree if it looks promising */
                gap_end = vm_start_gap(vma);
                if (gap_end >= low_limit && vma->vm_rb.rb_left) {
                        struct vm_area_struct *left =
                                rb_entry(vma->vm_rb.rb_left,
                                         struct vm_area_struct, vm_rb);
                        if (left->rb_subtree_gap >= length) {
                                vma = left;
                                continue;
                        }
                }

augmented rbtrees를 사용하여 구현된 함수로 유저 공간의 요청 범위에서 아래에서 위로 검색하여 vma 영역들 사이의 매핑되지 않은 빈(gap) 영역들 중 요청 길이가 들어갈 수 있는 빈(gap) 영역의 가상 시작 주소를 알아온다.

  • 코드 라인 16~18에서 align_mask를 추가한 길이를 구하고, 길이가 시스템 주소 영역을 초과하는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 21~23에서 high_limit을 산출한다. 범위를 벗어나는 경우 -ENOMEM 에러를 반환한다.
    • 기존 high_limit 에서 길이만큼 뺸 값
  • 코드 라인 25~27에서 low_limit을 산출한다. 범위를 벗어나는 경우 -ENOMEM 에러를 반환한다.
    • 기존 low_limit 에서 길이만큼 추가한 값
  • 코드 라인 30~31에서 메모리 디스크립터의 rb 트리가 비어있는 경우 check_highest 레이블로 이동한다.
  • 코드 라인 32~34에서 메모리 디스크립터의 루트 노드에서 vma를 가져온다. 만일 루트 노드의 rb_subtree_gap이 길이보다 작은 경우 루트 노드 이하의 노드들에는 빈 공간이 없으므로 최상위 공간을 체크하기 위해 check_highest 레이블로 이동한다.
    • 루트 노드의 rb_subtree_gap이 length보다 작은 경우 즉, 각 vma 간 gap이 length보다 작아 그 사이에 추가할 수 없으므로 최상위 영역을 조사한다.
  • 코드 라인 36~47에서 현재 태스크의 가상 메모리 영역 vma 들을 대상으로 루프를 돌며 rb 트리에서 좌측 노드 방향으로 길이 만큼의 gap 공간이 발견되는 노드를 찾는다.

 

mm/mmap.c -2/2-

                gap_start = vma->vm_prev ? vm_end_gap(vma->vm_prev) : 0;
check_current:
                /* Check if current node has a suitable gap */
                if (gap_start > high_limit)
                        return -ENOMEM;
                if (gap_end >= low_limit && 
                    gap_end > gap_start && gap_end - gap_start >= length)
                        goto found;

                /* Visit right subtree if it looks promising */
                if (vma->vm_rb.rb_right) {
                        struct vm_area_struct *right =
                                rb_entry(vma->vm_rb.rb_right,
                                         struct vm_area_struct, vm_rb);
                        if (right->rb_subtree_gap >= length) {
                                vma = right;
                                continue;
                        }
                }

                /* Go back up the rbtree to find next candidate node */
                while (true) {
                        struct rb_node *prev = &vma->vm_rb;
                        if (!rb_parent(prev))
                                goto check_highest;
                        vma = rb_entry(rb_parent(prev),
                                       struct vm_area_struct, vm_rb);
                        if (prev == vma->vm_rb.rb_left) {
                                gap_start = vm_end_gap(vma->vm_prev);
                                gap_end = vm_start_gap(vma);
                                goto check_current;
                        }
                }
        }

check_highest:
        /* Check highest gap, which does not precede any rbtree node */
        gap_start = mm->highest_vm_end;
        gap_end = ULONG_MAX;  /* Only for VM_BUG_ON below */
        if (gap_start > high_limit)
                return -ENOMEM;

found:
        /* We found a suitable gap. Clip it with the original low_limit. */
        if (gap_start < info->low_limit)
                gap_start = info->low_limit;

        /* Adjust gap address to the desired alignment */
        gap_start += (info->align_offset - gap_start) & info->align_mask;

        VM_BUG_ON(gap_start + info->length > info->high_limit);
        VM_BUG_ON(gap_start + info->length > gap_end);
        return gap_start;
}
  • 코드 라인 1에서 gap 시작 주소를 산출한다.
  • 코드 라인 2~5에서 check_current: 레이블이다. gap 시작 주소가 high_limit을 벗어나는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 6~8에서 gap_end가 low_limit 이상이면서 gap 영역이 길이 보다 큰 경우 found 레이블로 이동한다.
  • 코드 라인 11~19에서 이 번에는 반대로 노드의 하위 우측 노드가 있고 그 노드의 gap 영역이 길이 보다 큰 경우 그 우측 노드를 vma 영역에 대입하고 루프를 계속 진행한다.
  • 코드 라인 22~33에서 하위에 적당한 gap 영역이 없는 경우 다시 상위 노드로 올라가는데 좌측 노드에서 상위 노드로 올라간 경우라면 gap 시작과 끝을 갱신하고 check_current 레이블로 이동한다.
  • 코드 라인 36~41에서 check_highest: 레이블이다. 이 곳에서는 rb 트리의 빈 영역에 필요한 공간이 없어 그 상위의 빈자리를 찾는데 이 곳에서도 적절한 공간이 확보되지 않으면 -ENOMEM 에러를 반환한다.
  • 코드 라인 43~46에서 found: 레이블이다. 찾은 gap의 시작 위치를 최소한 info->low_limit으로 조절한다.
  • 코드 라인 49~53에서 gap의 시작 위치는 조절된 gap offset 만큼을 추가한 후 info->align_mask로 절삭하여 조절하고 반환한다.
    • file 및 캐시 aliasing으로 인해 찾은 언매핑 영역(gap)의 시작위치가 조절될 수 있다.

 

 

vm_start_gap()

include/linux/mm.h

static inline unsigned long vm_start_gap(struct vm_area_struct *vma)
{
        unsigned long vm_start = vma->vm_start;

        if (vma->vm_flags & VM_GROWSDOWN) {
                vm_start -= stack_guard_gap;
                if (vm_start > vma->vm_start)
                        vm_start = 0;
        }
        return vm_start;
}

스택 가드 갭이 적용된 vma 시작 주소를 반환한다.

 

vm_end_gap()

include/linux/mm.h

static inline unsigned long vm_end_gap(struct vm_area_struct *vma)
{
        unsigned long vm_end = vma->vm_end;

        if (vma->vm_flags & VM_GROWSUP) {
                vm_end += stack_guard_gap;
                if (vm_end < vma->vm_end)
                        vm_end = -PAGE_SIZE;
        }
        return vm_end;
}

스택 가드 갭이 적용된 vma 끝 주소를 반환한다.

 


Augmented rbtrees

레드 블랙 트리를 확장하여 augmented rbtree를 사용할 수 있는데 노드들에 대해 추가 정보를 저장할 수 있다. 노드에 대해 전파(propagation), 복사(copy), 회전(rotation)에 대한 콜백 함수를 사용자가 작성하여 등록하면 노드에 대해 변경이 일어날 때 마다 호출되어 추가적인 정보의 관리가 가능하게 만들어졌다. 커널에서도 vma 영역을 rbtree로 관리하는데 augmented rbtrees 방식이 도입되어 매핑되지 않은 빈 영역을 찾을 때 사용하기 위해 서브 트리들이 관리하는 gap 영역에 대한 추가 정보를 관리한다.

 

아래 그림은 각 노드가 하위 노드들에서 가장 큰 gap 정보를 선택하여 관리하고 있는 모습을 보여준다.

 

최대 VM_LOCKED 페이지 수 제한

mlock_future_check()

mm/mmap.c

static inline int mlock_future_check(struct mm_struct *mm,
                                     unsigned long flags,
                                     unsigned long len)
{
        unsigned long locked, lock_limit;

        /*  mlock MCL_FUTURE? */
        if (flags & VM_LOCKED) {
                locked = len >> PAGE_SHIFT;
                locked += mm->locked_vm;
                lock_limit = rlimit(RLIMIT_MEMLOCK);
                lock_limit >>= PAGE_SHIFT;
                if (locked > lock_limit && !capable(CAP_IPC_LOCK))
                        return -EAGAIN;
        }
        return 0;
}

현재 태스크에서 VM_LOCKED 플래그를 사용한 페이지들의 수가 최대 locked 페이지 수 제한을 초과하는 경우 -EAGAIN 에러를 반환한다. 그 외 성공리에 0을 반환한다.

  • VM_LOCKED된 매핑 영역은 페이지 회수(compaction 및 reclaim) 시스템에서 skip 한다.

 

참고

Early ioremap

<kernel v5.0>

Early IO 매핑

커널 부트업 과정에서 paging_init( ) 함수가 수행된 후에 메모리 및 입출력(I/O) 장치들이 가상 주소 공간에 정식으로 매핑되어 사용될 수 있다. 그런데 이러한 동작이 완료되기 전에 반드시 먼저 매핑되어 사용되어야 하는 경우를 위해 먼저(early) 처리할 수 있는 방법을 준비했다. 이러한 API를 사용하는 경우는 특정 시스템의 바이오스 정보 등에 먼저 접근해서 각종 메모리 및 디바이스의 정보를 가져와야 매핑 처리가 가능한 경우다. 주로 이용하는 경우는 다음과 같다.

  • ACPI 테이블에 접근해서 각종 디바이스의 설정 정보를 가져와야 하는 경우
  • EFI 테이블에 접근해서 각종 디바이스의 설정 정보를 가져와야 하는 경우
  • 일부 디바이스에서 특정 설정 정보 등을 읽어와야 하는 경우

 

이른(early) 시간에 io 매핑이 필요한 경우 256K씩 최대 7개의 fixmap 매핑 공간을 사용하여 매핑을 할 수 있게 한다.

 

다음 그림은 early_ioremap() 및 early_ioremap_init() 두 함수의 호출 관계를 보여준다.

 

Early IO 매핑 초기화

early_ioremap_init() – ARM32 & ARM64

ioremap을 사용하기 전에 부트 타임에 early I/O 매핑을 할 수 있도록 준비하는 과정을 자세히 알아본다. 다음 함수는 곧바로 early_ioremap_setup( ) 함수를 호출한다.

arch/arm64/mm/ioremap.c

/*
 * Must be called after early_fixmap_init
 */
void __init early_ioremap_init(void)
{
        early_ioremap_setup();
}

paging_init()이 완료되지 않아 정규 ioremap() 함수를 사용하지 못하지만 이른(early) 시간에 임시적으로 매핑이 필요한  경우를 위해 준비한다.

  • fixmap을 이용하므로 early_fixmap_init() 함수가 먼저 호출되어 초기화가 되어야한다.

 

early_ioremap_setup()

early I/O 매핑을 위해 fixmap 영역에 7개의 256K 가상 주소 영역을 준비하는 과정을 알아본다.

mm/early_ioremap.c

void __init early_ioremap_setup(void)
{
        int i;

        for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
                if (WARN_ON(prev_map[i]))
                        break;

        for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
                slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}

7개 배열로 이루어진 slot_virt[] 배열에 fixmap에서 early ioremap 용도로 배정된 가상 주소를 배정한다.

  • 코드 라인 5~7에서 FIX_BTMAPS_SLOTS(7) 수만큼 루프를 돌며 전역 prev_map[ ] 배열에 값이 설정된 경우 경고 메시지를 출력한다
    • 7개 각 주소 공간은 최대 256K를 사용할 수 있다.
  • 코드 라인 9~10에서 FIX_BTMAPS_SLOTS(7) 수만큼 루프를 돌며 전역 slot_virt[ ] 배열에 fixmap의 BTMAP 가상 주소를 설정한다. 7개의 fixmap 엔트리가 있고, 각 엔트리들은 fixmap의 BTMAP에 해당하는 가상 주소들을 가리키는데 엔트리 간의 간격은 256K이다.
    • slot_virt[0] = FIX_BTMAP_BEGIN
    • slot_virt[1] = FIX_BTMAP_BEGIN + 256K

 

다음 그림은 early_ioremap_setup( ) 함수가 호출되어 early IO 매핑에 사용되는 fixmap의 BTMAPS 영역이 7개의 slot이 초기화되는 것을 보여준다.

 

주요 API

early_ioremap()

mm/early_ioremap.c

/* Remap an IO device */
void __init __iomem *
early_ioremap(resource_size_t phys_addr, unsigned long size)
{
        return __early_ioremap(phys_addr, size, FIXMAP_PAGE_IO);
}

물리 주소 @phys_addr부터 @size에 대응하는 io 디바이스들을 early 매핑한 후 매핑된 가상 주소를 반환한다.

  • 256K 크기를 최대 7개 까지 io 디바이스를 early 매핑한다.
  • FIXMAP_PAGE_IO
    • ARM32
      • L_PTE_YOUNG | L_PTE_PRESENT | L_PTE_XN | L_PTE_DIRTY | L_PTE_MT_DEV_SHARED | L_PTE_SHARED 송성을 가진다.
    •  ARM64
      • PROT_DEVICE_nGnRE 매핑 속성을 가진다.

 

early_memremap()

mm/early_ioremap.c

/* Remap memory */ 
void __init *
early_memremap(resource_size_t phys_addr, unsigned long size)
{
        return (__force void *)__early_ioremap(phys_addr, size,
                                               FIXMAP_PAGE_NORMAL);
}

물리 주소 @phys_addr부터 @size에 대응하는 메모리 영역을 early 매핑한 후 매핑된 가상 주소를 반환한다.

  • 256K 크기를 최대 7개 까지 메모리 영역을 early 매핑한다.
  • FIXMAP_PAGE_NORMAL
    • ARM32
      • L_PTE_XN 속성이 추가된 커널 메모리 속성이다.
    • ARM64
      • 커널 메모리 속성과 동일하다.

 

__early_ioremap()

mm/early_ioremap.c

static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
        unsigned long offset;
        resource_size_t last_addr;
        unsigned int nrpages;
        enum fixed_addresses idx;
        int i, slot;

        WARN_ON(system_state != SYSTEM_RUNNING);

        slot = -1;
        for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
                if (!prev_map[i]) {
                        slot = i;
                        break;
                }
        }

        if (WARN(slot < 0, "%s(%08llx, %08lx) not found slot\n",
                 __func__, (u64)phys_addr, size))
                return NULL;

        /* Don't allow wraparound or zero size */
        last_addr = phys_addr + size - 1;
        if (WARN_ON(!size || last_addr < phys_addr))
                return NULL;

        prev_size[slot] = size;
        /*
         * Mappings have to be page-aligned
         */
        offset = offset_in_page(phys_addr);
        phys_addr &= PAGE_MASK;
        size = PAGE_ALIGN(last_addr + 1) - phys_addr;

        /*
         * Mappings have to fit in the FIX_BTMAP area.
         */
        nrpages = size >> PAGE_SHIFT;
        if (WARN_ON(nrpages > NR_FIX_BTMAPS))
                return NULL;

        /*
         * Ok, go for it..
         */
        idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
        while (nrpages > 0) {
                if (after_paging_init)
                        __late_set_fixmap(idx, phys_addr, prot);
                else
                        __early_set_fixmap(idx, phys_addr, prot);
                phys_addr += PAGE_SIZE;
                --idx;
                --nrpages;
        }
        WARN(early_ioremap_debug, "%s(%08llx, %08lx) [%d] => %08lx + %08lx\n",
             __func__, (u64)phys_addr, size, slot, offset, slot_virt[slot]);

        prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
        return prev_map[slot];
}

요청한 물리 주소와 사이즈를 fixmap의 BTMAPS 슬롯에 해당하는 가상주소에 prot 속성으로 매핑한다. 실패하는 경우 null을 반환한다. arm64에서 early 하게 io 매핑할 수 있는 최대 수는 FIX_BTMAPS_SLOTS(7)개이다.

  • fixmap용 BTMAPS 슬롯 관리 정보
    • slot_virt[]: 부트업 타임에 초기화된 각 슬롯에 해당하는 fixmap 가상 주소 (256K 단위)
    • prev_map[]: fixmap에 매핑된 가상 주소
    • prev_size[]: 매핑된 사이즈

 

  • 코드 라인 12-18에서 사용할 빈 슬롯을 선택한다. FIX_BTMAPS_SLOTS(7) 수만큼 루프를 돌며 전역 prev_map[ ] 값이 설정되지 않았다면 현재 카운터 i 값을 slot에 설정하여 slot을 선택한다.
  • 코드 라인 20~22에서 루프를 도는 동안 빈 슬롯을 못 찾은 경우 경고 메시지를 출력하고 NULL 값을 리턴한다
  • 코드 라인 25~27에서 크기가 0이거나 끝 주소가 시스템 주소 범위를 벗어나는 경우 경고 메시지를 출력하고 NULL 값을 리턴한다.
  • 코드 라인 29~34에서 prev_size[ ]에 요청 크기를 담아둔다. offset은 요청 물리 주소에서 페이지 단위의 나머지 offset만을 계산하고, 물리 주소는 페이지 단위로 내림 정렬한다
  • 코드 라인 35에서 페이지 단위로 정렬된 크기 값을 계산한다. 처음 요청한 물리 시작 주소 + 크기 값을 페이지 단위로 정렬하고, 페이지 단위로 내림 정렬한 물리 주소를 뺀다(최솟값은 페이지 단위가 된다).
  • 코드 라인 40~42에서 크기 값으로 페이지 수를 계산하고 한다. 크기가 256K를 초과한다면 NULL을 리턴한다
  • 코드 라인 48에서 계산된 페이지 수만큼 루프를 돈다.
  • 코드 라인 49~50에서 전역 after_paging_init이 설정된 경우에는 _ _late_clear_fixmap( ) 매크로 함수를 호출한다.
    • 예) arm64 및 x86의 경우 별도의 late 처리 함수 없이 그냥 __set_fixmap() 함수를 호출한다.
  • 코드 라인 51~52에서 반대로 after_paging_init이 설정되지 않은 경우에는 _ _early_set_fixmap( ) 매크로 함수를 호출한다.
    • 예) arm64의 경우 별도의 early 처리 함수 없이 그냥 __set_fixmap() 함수를 호출한다.
    • 예) x64의 경우 별도의 early 처리 함수인 __early_set_fixmap() 함수를 호출한다.
  • 코드 라인 60에서 prev_map[slot]에 슬롯에 해당하는 fixmap 가상 주소 + offset을 더해 설정한다

 

__early_set_fixmap()

arm64/include/asm/fixmap.h

#define __early_set_fixmap __set_fixmap

paging_init()이 완료되기 전에는 early_ioremap() 함수가 호출되는 경우에는 fixmap에 매핑한다.

 

__late_set_fixmap()

mm/early_ioremap.c

#define __late_set_fixmap __set_fixmap

paging_init()이 완료된 후 early_ioremap() 함수가 호출되는 경우에는 fixmap에 매핑한다.

 


early_ioremap() API 제공 만료

early_ioremap_reset()

mm/early_ioremap.c

void __init early_ioremap_reset(void)
{
        early_ioremap_shutdown();
        after_paging_init = 1;
}

정규 페이징이 준비되었으므로 early_ioremap() API를 더 이상 사용하지 말고 정규 ioremap()을 사용해야 하는 시점이다.

  • 아키텍처에 따라 이 함수 진행 후에 early_ioremap() 함수가 호출될 때 BUG() 발생토록 한다.
    • x86과 arm64의 경우에는 계속 early_ioremap() 함수의 사용을 허용한다.

 

기타 API

  • early_memremap_ro()
    • early_memremap()과 동일하나 read only 속성이 추가된다.
  • early_memremap_prot()
    • early_memremap()과 동일하나 @prot_val 속성을 요청하여 추가할 수 있다.

 

참고

Ioremap

 

io 매핑

 

정규 ioremap 함수는 빈 vmalloc 영역을 찾아 매핑하는데 매핑 종류에 따라 5개의 함수를 사용한다. 아키텍처마다 지원되는 캐시의 종류가 다르므로 특정 모드가 지원되지  않는 경우 하향 호환을 시킨다. ioremap_cache() 함수는 캐시를 사용하지만 아키텍처에 따라 읽기 캐시외에 쓰기 캐시까지도 사용할 수 있다. 쓰기 캐시에 대한 옵션을 가능하면 선택할 수 있도록 하기 위해 특정 아키텍처에서는 ioremap_wc() 함수와 ioremap_wt() 함수를 구분하여 사용하기도 한다. 참고로 다음 표와 같이 arm 및 arm64 에서 원래 함수 의도와는 다르게 약간 상이한 매핑 타입을 확인할 수 있다.

 

 

32bit arm ioremap 함수 흐름도

arm64 ioremap 함수 흐름도

ioremap() 등

arch/arm/include/asm/io.h

/*
 * ioremap and friends.
 *
 * ioremap takes a PCI memory address, as specified in
 * Documentation/io-mapping.txt.
 *
 */
#define ioremap(cookie,size)            __arm_ioremap((cookie), (size), MT_DEVICE)
#define ioremap_nocache(cookie,size)    __arm_ioremap((cookie), (size), MT_DEVICE)
#define ioremap_cache(cookie,size)      __arm_ioremap((cookie), (size), MT_DEVICE_CACHED)
#define ioremap_wc(cookie,size)         __arm_ioremap((cookie), (size), MT_DEVICE_WC)

arm64에서 ioremap(), ioremap_nocache(), ioremap_wt() 함수는 PROT_DEVICE_nGnRE 매핑을 사용하여 캐시를 사용하지 않는 디바이스 타입으로 매핑하고, ioremap_wc() 함수는 캐시를 사용하지 않는 것은 동일하지만 디바이스 타입이 아닌 메모리 타입으로 매핑하는 것이 다른 점이다.

 

__arm_ioremap()

arch/arm/mm/ioremap.c

void __iomem *
__arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
        return arch_ioremap_caller(phys_addr, size, mtype,
                __builtin_return_address(0));
}
EXPORT_SYMBOL(__arm_ioremap);

요청 물리주소 및 사이즈만큼 빈 vmalloc 공간을 찾아 mtype 속성으로 매핑하고 가상 주소를 반환한다.

 

전역 __arm_ioremap_caller 함수 포인터 변수

arch/arm/mm/ioremap.c

void __iomem * (*arch_ioremap_caller)(phys_addr_t, size_t,
                                      unsigned int, void *) =
        __arm_ioremap_caller;

컴파일 타임에 전역 arch_ioremap_caller 함수 포인터 변수에 __arm_ioremap_caller() 함수 주소를 대입한다.

 

__arm_ioremap_caller()

arch/arm/mm/ioremap.c

void __iomem *__arm_ioremap_caller(phys_addr_t phys_addr, size_t size,
        unsigned int mtype, void *caller)
{
        phys_addr_t last_addr;
        unsigned long offset = phys_addr & ~PAGE_MASK;
        unsigned long pfn = __phys_to_pfn(phys_addr);

        /*
         * Don't allow wraparound or zero size
         */
        last_addr = phys_addr + size - 1;
        if (!size || last_addr < phys_addr)
                return NULL;

        return __arm_ioremap_pfn_caller(pfn, offset, size, mtype,
                        caller);
}

요청 물리주소 및 사이즈만큼 빈 vmalloc 공간을 찾아 mtype 속성으로 매핑하고 가상 주소를 반환한다.

  • 코드 라인 5~6에서 물리 주소에서 페이지 단위로 절삭하여 남은 offset 값과 pfn 값을 구한다.
  • 코드 라인 11에서 페이지 단위로 매핑할 페이지의 끝 주소를 구한다.
    • 예) phys_addr=0x1234_5000, size=0x2000
      • last_addr=0x1234_6fff
  • 코드 라인 12~13에서 size가 0이거나 끝 주소가 시스템 범위를 초과하는 경우 null을 반환한다.
  • 코드 라인 15에서 pfn에 대해 size 만큼 빈 vmalloc 공간을 찾아 mtype 속성으로 매핑하고 가상 주소를 반환한다.

 

__arm_ioremap_pfn_caller()

arch/arm/mm/ioremap.c

void __iomem * __arm_ioremap_pfn_caller(unsigned long pfn,
        unsigned long offset, size_t size, unsigned int mtype, void *caller)
{
        const struct mem_type *type;
        int err;
        unsigned long addr;
        struct vm_struct *area;
        phys_addr_t paddr = __pfn_to_phys(pfn);

#ifndef CONFIG_ARM_LPAE
        /*
         * High mappings must be supersection aligned
         */
        if (pfn >= 0x100000 && (paddr & ~SUPERSECTION_MASK))
                return NULL;
#endif

        type = get_mem_type(mtype);
        if (!type)
                return NULL;

        /*
         * Page align the mapping size, taking account of any offset.
         */
        size = PAGE_ALIGN(offset + size);

        /*
         * Try to reuse one of the static mapping whenever possible.
         */
        if (size && !(sizeof(phys_addr_t) == 4 && pfn >= 0x100000)) {
                struct static_vm *svm;

                svm = find_static_vm_paddr(paddr, size, mtype);
                if (svm) {
                        addr = (unsigned long)svm->vm.addr;
                        addr += paddr - svm->vm.phys_addr;
                        return (void __iomem *) (offset + addr);
                }
        }

        /*
         * Don't allow RAM to be mapped - this causes problems with ARMv6+
         */
        if (WARN_ON(pfn_valid(pfn)))
                return NULL;

        area = get_vm_area_caller(size, VM_IOREMAP, caller);
        if (!area)
                return NULL;
        addr = (unsigned long)area->addr;
        area->phys_addr = paddr;

#if !defined(CONFIG_SMP) && !defined(CONFIG_ARM_LPAE)
        if (DOMAIN_IO == 0 &&
            (((cpu_architecture() >= CPU_ARCH_ARMv6) && (get_cr() & CR_XP)) ||
               cpu_is_xsc3()) && pfn >= 0x100000 &&
               !((paddr | size | addr) & ~SUPERSECTION_MASK)) {
                area->flags |= VM_ARM_SECTION_MAPPING;
                err = remap_area_supersections(addr, pfn, size, type);
        } else if (!((paddr | size | addr) & ~PMD_MASK)) {
                area->flags |= VM_ARM_SECTION_MAPPING;
                err = remap_area_sections(addr, pfn, size, type);
        } else
#endif
                err = ioremap_page_range(addr, addr + size, paddr,
                                         __pgprot(type->prot_pte));

        if (err) {
                vunmap((void *)addr);
                return NULL;
        }

        flush_cache_vmap(addr, addr + size);
        return (void __iomem *) (offset + addr);
}

요청 물리주소 및 사이즈만큼 빈 vmalloc 공간을 찾아 mtype 속성으로 매핑하고 가상 주소를 반환한다.

  • 코드 라인 18~20에서 요청한 mtype에 해당하는 mem_type 구조체를 알아오는데 없는 경우 null을 반환한다.
  • 코드 라인 25에서 매핑에 필요한 페이지 사이즈를 구하기 위해 size에 offset을 더한 값을 페이지 단위로 정렬한다.
    • 예) offset=0x678, size=0x1000, PAGE_SIZE=4K
      • size=0x2000 (2개 페이지)
  • 코드 라인 30에서  물리 주소가 4이면서 pfn이 0x10000(4G)를 초과하는 경우만 아니라면
  • 코드 라인 33~38에서 vmalloc 공간에 이미 동일한 타입으로 static 매핑 영역이내에 포함된 경우 매핑 없이 가상 주소만 찾아 반환한다.
  • 코드 라인 47~49에서 VM_IOREMAP 플래그를 사용하여 vmalloc 공간에서 빈 영역을 찾아 영역 정보를 vm_struct 구조체 형태로 알아오고 null인 경우 함수를 빠져나간다.
  • 코드 라인 50~51에서 찾은 시작 가상 주소를 addr에 대입하고 영역의 시작 물리 주소에 paddr을 대입한다.
  • 코드 라인 53~64에서 CONFIG_SMP 커널 옵션을 사용하지 않고 LPAE도 아닌 경우 수퍼 섹션 또는 섹션 매핑을 지원한다.
  • 코드 라인 65~66에서 가상 주소 범위만큼 물리 주소 paddr부터 매핑한다.
  • 코드 라인 68~71 매핑이 실패하는 경우 매핑해제를 시도한다.
  • 코드 라인 73에서 매핑된 가상 주소 영역을 flush한다. 단 armv7 및 armv8 아키텍처와 같이 데이터 캐시가 PIPT 또는 VIPT non-aliasing을 사용하는 경우에는 flush할 필요없다

 

find_static_vm_paddr()

arch/arm/mm/ioremap.c

static struct static_vm *find_static_vm_paddr(phys_addr_t paddr,
                        size_t size, unsigned int mtype)
{
        struct static_vm *svm;
        struct vm_struct *vm;

        list_for_each_entry(svm, &static_vmlist, list) {
                vm = &svm->vm;
                if (!(vm->flags & VM_ARM_STATIC_MAPPING))
                        continue;
                if ((vm->flags & VM_ARM_MTYPE_MASK) != VM_ARM_MTYPE(mtype))
                        continue;

                if (vm->phys_addr > paddr ||
                        paddr + size - 1 > vm->phys_addr + vm->size - 1)
                        continue;

                return svm;
        }

        return NULL;
}

요청 영역이 vmalloc 공간에 매핑된 영역 엔트리 중 하나에 포함되고 같은 매핑 타입으로 이미 static 매핑된 경우 해당 vm_struct 정보를 반환한다. 그 외의 경우 null을 반환한다.

  • 코드 라인 7에서 전역 static_vmlist의 모든 엔트리를 대상으로 루프를 돈다.
  • 코드 라인 9~10에서 해당 영역이 VM_ARM_STATIC_MAPPING 플래그를 사용하지 않으면 skip 한다.
  • 코드 라인 11~12에서 해당 영역에 매핑된 타입과 요청 매핑 타입이 다른 경우 skip 한다.
  • 코드 라인 14~16에서 기존 영역의 범위내에 요청 범위가 포함되지 않은 경우 skip 한다.

 

remap_area_sections()

arch/arm/mm/ioremap.c

static int
remap_area_sections(unsigned long virt, unsigned long pfn,
                    size_t size, const struct mem_type *type)
{
        unsigned long addr = virt, end = virt + size;
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;

        /*
         * Remove and free any PTE-based mapping, and
         * sync the current kernel mapping.
         */
        unmap_area_sections(virt, size);

        pgd = pgd_offset_k(addr);
        pud = pud_offset(pgd, addr);
        pmd = pmd_offset(pud, addr);
        do {
                pmd[0] = __pmd(__pfn_to_phys(pfn) | type->prot_sect);
                pfn += SZ_1M >> PAGE_SHIFT;
                pmd[1] = __pmd(__pfn_to_phys(pfn) | type->prot_sect);
                pfn += SZ_1M >> PAGE_SHIFT;
                flush_pmd_entry(pmd);

                addr += PMD_SIZE;
                pmd += 2;
        } while (addr < end);

        return 0;
}

1M 단위 섹션 매핑이 가능한 경우 해당 pmd 엔트리를 섹션 페이지에 매핑한다.

  • arm에서 pmd 엔트리는 pmd[0] 및 pmd[1] 2개로 구성되어 있다.

 

unmap_area_sections()

lib/ioremap.c

/*
 * Section support is unsafe on SMP - If you iounmap and ioremap a region,
 * the other CPUs will not see this change until their next context switch.
 * Meanwhile, (eg) if an interrupt comes in on one of those other CPUs
 * which requires the new ioremap'd region to be referenced, the CPU will
 * reference the _old_ region.
 *
 * Note that get_vm_area_caller() allocates a guard 4K page, so we need to
 * mask the size back to 1MB aligned or we will overflow in the loop below.
 */
static void unmap_area_sections(unsigned long virt, unsigned long size)
{
        unsigned long addr = virt, end = virt + (size & ~(SZ_1M - 1));
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmdp;

        flush_cache_vunmap(addr, end);
        pgd = pgd_offset_k(addr);
        pud = pud_offset(pgd, addr);
        pmdp = pmd_offset(pud, addr);
        do {
                pmd_t pmd = *pmdp;

                if (!pmd_none(pmd)) {
                        /*
                         * Clear the PMD from the page table, and
                         * increment the vmalloc sequence so others
                         * notice this change.
                         *
                         * Note: this is still racy on SMP machines.
                         */
                        pmd_clear(pmdp);
                        init_mm.context.vmalloc_seq++;

                        /*
                         * Free the page table, if there was one.
                         */
                        if ((pmd_val(pmd) & PMD_TYPE_MASK) == PMD_TYPE_TABLE)
                                pte_free_kernel(&init_mm, pmd_page_vaddr(pmd));
                }

                addr += PMD_SIZE;
                pmdp += 2;
        } while (addr < end);

        /*
         * Ensure that the active_mm is up to date - we want to
         * catch any use-after-iounmap cases.
         */
        if (current->active_mm->context.vmalloc_seq != init_mm.context.vmalloc_seq)
                __check_vmalloc_seq(current->active_mm);

        flush_tlb_kernel_range(virt, end);
}

vmalloc 공간에서 pmd 매핑된 엔트리를 클리어한다.

  • 코드 라인 18에서 요청 영역을 pmd 섹션 단위만큼  데이터 캐시를 flush한다. 단 armv7 및 armv8 아키텍처와 같이 데이터 캐시가 PIPT 또는 VIPT non-aliasing을 사용하는 경우에는 flush할 필요없다
  • 코드 라인 19~21에서 커널 페이지 테이블을 통해 요청 가상 주소에 해당하는 pgd, pud, pmd 엔트리 주소를 알아온다.
  • 코드 라인 25~33에서 이미 pmd 매핑된 경우 매핑을 클리어한다.
  • 코드 라인 34에서 커널에 메모리 context에서 vmalloc 시퀀스 값을 증가시켜 커널의 vmalloc 등록 정보가 갱신되었음을 나타내게 한다.
  • 코드 라인 39~40에서 기존 pmd 엔트리가 테이블을 가리킨 경우 그 테이블을 할당 해제 시킨다.
  • 코드 라인 43~45에서 끝 주소까지 pmd 단위를 증가시키며 루프를 돈다.
  • 코드 라인 51~52에서 커널의 vmalloc 정보와 현재 태스크의 vmalloc 정보를 비교하여 갱신된 경우 커널의 pgd 매핑된 vmalloc 엔트리들을 현재 태스크 메모리 디스크립터의 pgd 테이블에 복사한다.
  • 코드 라인 54에서 해당 커널 영역에 대한 TLB cache를 flush 한다

 

ioremap_page_range()

lib/ioremap.c

int ioremap_page_range(unsigned long addr, 
                       unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
        pgd_t *pgd;
        unsigned long start;
        unsigned long next;
        int err;

        BUG_ON(addr >= end);

        start = addr;
        phys_addr -= addr;
        pgd = pgd_offset_k(addr);
        do {
                next = pgd_addr_end(addr, end);
                err = ioremap_pud_range(pgd, addr, next, phys_addr+addr, prot);
                if (err)
                        break;
        } while (pgd++, addr = next, addr != end);

        flush_cache_vmap(start, end);

        return err;
}
EXPORT_SYMBOL_GPL(ioremap_page_range);

주어진 가상 주소 영역에 물리 주소를 prot 속성으로 매핑한다.

  • 코드 라인 11~12에서 요청 가상 주소를 start에 잠시 보관하고, 물리 주소에서 요청 가상 주소를 뺀다.
    • 예) addr=0x1000_0000, phys_addr=0x1234_5000
      • phys_addr=0x0234_5000
  • 코드 라인 13에서 가상 주소 addr에 해당하는 커널 pgd 엔트리 주소를 알아온다.
  • 코드 라인 15에서 다음 pgd 엔트리가 관리하는 가상 주소를 가져오되 마지막인 경우 end 값을 가져온다.
  • 코드 라인 16~18에서 pgd 엔트리 하나 범위내 addr ~ next 가상 주소 범위의 pud 엔트리들을 매핑하고 에러인 경우 루프를 탈출한다.
  • 코드 라인 19에서 다음 pgd 엔트리 및 다음 가상 주소를 선택하고  end까지 루프를 돌며 반복한다.
  • 코드 라인 21에서 매핑된 가상 주소 영역을 flush한다. 단 armv7 및 armv8 아키텍처와 같이 데이터 캐시가 PIPT 또는 VIPT non-aliasing을 사용하는 경우에는 flush할 필요없다.

 

ioremap_pud_range()

lib/ioremap.c

static inline int ioremap_pud_range(pgd_t *pgd, unsigned long addr,
                unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
        pud_t *pud;
        unsigned long next;

        phys_addr -= addr;
        pud = pud_alloc(&init_mm, pgd, addr);
        if (!pud)
                return -ENOMEM;
        do {
                next = pud_addr_end(addr, end);
                if (ioremap_pmd_range(pud, addr, next, phys_addr + addr, prot))
                        return -ENOMEM;
        } while (pud++, addr = next, addr != end);
        return 0;          
}

pud 단위로 주어진 가상 주소 영역에 물리 주소를 prot 속성으로 매핑한다.

  • 코드 라인 7에서 물리 주소에서 요청 가상 주소를 미리 뺀다.
    • 예) addr=0x1000_0000, phys_addr=0x1_1234_5000
      • phys_addr=0x1_0234_5000
  • 코드 라인 8~10에서 pud용 테이블을 하나 할당 받아온다. 만일 실패하는 경우 -ENOMEM 에러로 함수를 빠져나간다.
  • 코드 라인 12에서 다음 pud 엔트리가 관리하는 가상 주소를 가져오되 마지막인 경우 end 값을 가져온다.
  • 코드 라인 13~14에서 pud 엔트리 하나 범위내 addr ~ next 가상 주소 범위의 pmd 엔트리들을 매핑한다. 실패하는 경우 -ENOMEM 결과를 가지고 함수를 빠져나간다.
  • 코드 섹션 15에서 다음 pud 엔트리 및 다음 가상 주소를 선택하고  end까지 루프를 돌며 반복한다.

 

ioremap_pmd_range()

lib/ioremap.c

static inline int ioremap_pmd_range(pud_t *pud, unsigned long addr,
                unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
        pmd_t *pmd;
        unsigned long next;

        phys_addr -= addr;
        pmd = pmd_alloc(&init_mm, pud, addr);
        if (!pmd)
                return -ENOMEM;
        do {
                next = pmd_addr_end(addr, end);
                if (ioremap_pte_range(pmd, addr, next, phys_addr + addr, prot))
                        return -ENOMEM;
        } while (pmd++, addr = next, addr != end); 
        return 0;              
}

pmd 단위로 주어진 가상 주소 영역에 물리 주소를 prot 속성으로 매핑한다.

  • 코드 라인 7에서 물리 주소에서 요청 가상 주소를 미리 뺀다.
  • 코드 라인 8~10에서 pmd용 테이블을 하나 할당 받아온다. 만일 실패하는 경우 -ENOMEM 에러로 함수를 빠져나간다.
  • 코드 라인 12에서 다음 pmd 엔트리가 관리하는 가상 주소를 가져오되 마지막인 경우 end 값을 가져온다.
  • 코드 라인 13~14에서 pmd 엔트리 하나 범위내 addr ~ next 가상 주소 범위의 pte 엔트리들을 매핑한다. 실패하는 경우 -ENOMEM 결과를 가지고 함수를 빠져나간다.
  • 코드 섹션 15에서 다음 pmd 엔트리 및 다음 가상 주소를 선택하고  end까지 루프를 돌며 반복한다.

 

ioremap_pte_range()

lib/ioremap.c

static int ioremap_pte_range(pmd_t *pmd, unsigned long addr,
                unsigned long end, phys_addr_t phys_addr, pgprot_t prot)
{
        pte_t *pte;
        u64 pfn;

        pfn = phys_addr >> PAGE_SHIFT;
        pte = pte_alloc_kernel(pmd, addr);
        if (!pte)
                return -ENOMEM;
        do {
                BUG_ON(!pte_none(*pte));
                set_pte_at(&init_mm, addr, pte, pfn_pte(pfn, prot));
                pfn++;
        } while (pte++, addr += PAGE_SIZE, addr != end);
        return 0;
}

pte 단위로 주어진 가상 주소 영역에 물리 주소 페이지를 prot 속성으로 매핑한다.

  • 코드 라인 7에서 pfn을 알아온다.
  • 코드 라인 8~10에서 pte용 테이블을 하나 할당 받아온다. 만일 실패하는 경우 -ENOMEM 에러로 함수를 빠져나간다.
  • 코드 라인 13~14에서 가상 주소 addr에 해당하는 pte 엔트리를 pfn 물리페이지에 prot 타입으로 매핑시키고 pfn을 증가시킨다.
  • 코드 라인 15에서 다음 pte 엔트리를 증가시키고, 가상 주소도 다음 페이지만큼 증가시키며 end까지 루프를 돌며 반복한다.

 

io 언매핑

iounmap()

arch/arm/include/asm/io.h

#define iounmap                         __arm_iounmap

요청 물리 주소의 io 매핑을 해제한다.

 

__arm_iounmap()

arch/arm/mm/ioremap.c

void __arm_iounmap(volatile void __iomem *io_addr)
{
        arch_iounmap(io_addr);
}
EXPORT_SYMBOL(__arm_iounmap);

요청 물리 주소의 io 매핑을 해제한다.

 

arch_iounmap()

arch/arm/mm/ioremap.c

void (*arch_iounmap)(volatile void __iomem *) = __iounmap;

컴파일 타임에 전역 arch_iounmap 함수 포인터 변수에는 __iounmap() 함수의 주소가 담긴다.

 

__iounmap()

arch/arm/mm/ioremap.c

void __iounmap(volatile void __iomem *io_addr)
{       
        void *addr = (void *)(PAGE_MASK & (unsigned long)io_addr);
        struct static_vm *svm;

        /* If this is a static mapping, we must leave it alone */
        svm = find_static_vm_vaddr(addr);
        if (svm)
                return;

#if !defined(CONFIG_SMP) && !defined(CONFIG_ARM_LPAE)
        {
                struct vm_struct *vm;
                       
                vm = find_vm_area(addr);
        
                /*
                 * If this is a section based mapping we need to handle it
                 * specially as the VM subsystem does not know how to handle
                 * such a beast.
                 */
                if (vm && (vm->flags & VM_ARM_SECTION_MAPPING))
                        unmap_area_sections((unsigned long)vm->addr, vm->size);
        }
#endif          

        vunmap(addr);
}

요청 물리 주소의 io 매핑을 해제한다.

  • 코드 라인 7~9에서 vmalloc 공간에 요청 주소에 대한 static 매핑이 있는 경우 그냥 함수를 빠져나간다.
  • 코드 라인 11~25에서 시스템(빌드된 커널)이 SMP 및 LPAE를 지원하지 않는 경우  해당 주소로 섹션 매핑이 된 경우 해제한다.
  • 코드 라인 27에서 vmalloc 공간에 매핑된 io 물리 주소를 vunmap() 함수를 호출하여 매핑을 해제한다.

 

vmalloc 공간의 매핑 상태 확인

다음과 같이 어떤 api를 통해 물리주소가 vmalloc 가상 주소 공간에 매핑되었는지 확인할 수 있다.

$ sudo cat /proc/vmallocinfo
0xba800000-0xbb000000 8388608 iotable_init+0x0/0xb8 phys=3a800000 ioremap
0xbb804000-0xbb806000    8192 raw_init+0x50/0x148 pages=1 vmalloc
0xbb806000-0xbb809000   12288 pcpu_mem_zalloc+0x44/0x80 pages=2 vmalloc
0xbb899000-0xbb89c000   12288 pcpu_mem_zalloc+0x44/0x80 pages=2 vmalloc
0xbb89c000-0xbb89e000    8192 dwc_otg_driver_probe+0x650/0x7a8 phys=3f006000 ioremap
0xbb89e000-0xbb8a0000    8192 devm_ioremap_nocache+0x40/0x7c phys=3f300000 ioremap
0xbbc00000-0xbbc83000  536576 bcm2708_fb_set_par+0x108/0x13c phys=3db79000 ioremap
0xbc9f7000-0xbc9ff000   32768 SyS_swapon+0x618/0xf6c pages=7 vmalloc
0xbc9ff000-0xbca01000    8192 SyS_swapon+0x850/0xf6c pages=1 vmalloc
0xbcc39000-0xbcc3b000    8192 SyS_swapon+0xaa0/0xf6c pages=1 vmalloc
0xbce7b000-0xbce7f000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbce83000-0xbce87000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbce8b000-0xbce8f000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbce93000-0xbce97000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbce9b000-0xbce9f000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbcea7000-0xbceab000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbceab000-0xbceaf000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbceb7000-0xbceb9000    8192 bpf_prog_alloc+0x44/0xb0 pages=1 vmalloc
0xbf509000-0xbf50d000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xbf50d000-0xbf511000   16384 n_tty_open+0x20/0xe4 pages=3 vmalloc
0xf3000000-0xf3001000    4096 iotable_init+0x0/0xb8 phys=3f000000 ioremap
0xf3003000-0xf3004000    4096 iotable_init+0x0/0xb8 phys=3f003000 ioremap
0xf3007000-0xf3008000    4096 iotable_init+0x0/0xb8 phys=3f007000 ioremap
0xf300b000-0xf300c000    4096 iotable_init+0x0/0xb8 phys=3f00b000 ioremap
0xf3100000-0xf3101000    4096 iotable_init+0x0/0xb8 phys=3f100000 ioremap
0xf3200000-0xf3201000    4096 iotable_init+0x0/0xb8 phys=3f200000 ioremap
0xf3201000-0xf3202000    4096 iotable_init+0x0/0xb8 phys=3f201000 ioremap
0xf3215000-0xf3216000    4096 iotable_init+0x0/0xb8 phys=3f215000 ioremap
0xf3980000-0xf39a0000  131072 iotable_init+0x0/0xb8 phys=3f980000 ioremap
0xf4000000-0xf4001000    4096 iotable_init+0x0/0xb8 phys=40000000 ioremap
0xfea50000-0xff000000 5963776 pcpu_get_vm_areas+0x0/0x5e0 vmalloc

 

참고