Linux Kernel

準備工作

版本

環境

下載後先解壓縮

$ tar zxvf linux-3.9.1.tar.gz 

編譯

$ cd linux-3.9.1
$ make menuconfig # or make defconfig
$ make clean
$ make
$ make modules_install install

第一次make有噴error: include/linux/compiler-gcc.h:106:30: fatal error: linux/compiler-gcc5.h:No such file or directory
把當前kernel原碼中的gcc複製到要編譯的kernel裡即可
https://blog.csdn.net/u014525494/article/details/53573298

$ make -jn #指定用n個核心跑

$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 3.9.1 (root@eugene) #0 SMP Sun Nov 17 22:21:14 CST 2019, RO-rootFS, swap_dev 0x5, Normal VGA

Enable the kernel for boot(雖然好像他會自己加)

$ sudo update-initramfs -c -k 3.19
$ sudo update-grub

然後直接用這個kernel開機的話會卡在
Uncompressing Linux done, booting the kernel.
上網查了之後發現可能的原因一大堆,索性直接換了3.10.1看看結果還是一樣
另外發現在make install和update-initramfs的時候有噴amd64-microcode unsupported kernel version
最後改成4.4.1(本來的kernel是4.4.0)就好了

開機之後確定使用的kernel是我們剛剛編的

$ uname -a
Linux eugene 4.4.1 #1 SMP Mon Nov 18 14:36:16 CST 2019 x86_64 x86_64 x86_64 GNU/Linux

成功!

新增一個system call

  • 先建立一個程式裡面有sys_helloworld的function
$ cd linux-4.4.1/
$ mkdir mycall
$ vim helloworld.c

#include <linux/kernel.h>
asmlinkage int sys_helloworld(void){
        printk("hello world");
        return 0;
}
  • 建立Makefile
$ vim Makefile

obj-y := helloworld.o
  • 在主要的Makefile中新增mycall資料夾進去
$ cd ..
$ vim Makefile

core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ mycall/
  • 在system call table裡新增我們的system call
$ vim arch/x86/entry/syscalls/syscall_64.tbl

546 64 helloworld sys_helloworld # 在最後面一行加上
  • 修改 system call header
$ vim include/linux/syscalls.h

# 加在#endif前
asmlinkage int helloworld(void);
  • 編譯
$ sudo make
$ sudo make modules_install install
$ reboot

最後寫一個程式測試一下

#include <stdio.h>
#include <syscall.h>
#include <sys/types.h>

int main(){
        int a = syscall(546);
        printf("system call sys_hello_world return %d\n", a);
        return 0;
}

$ dmesg

有看到hello world就代表成功了

reference:
https://chybeta.github.io/2017/10/19/Linux-kernel-development-1-环境准备/
https://www.linux.com/tutorials/how-compile-linux-kernel-0/
https://wenyuangg.github.io/posts/linux/linux-add-system-call.html
https://blog.kaibro.tw/2016/11/07/Linux-Kernel編譯-Ubuntu/?fbclid=IwAR0n1xjghssrijlA7L8nFhjojsu-Wdb8w25900l_WVtvDQeJgJzv7MaXxIU

Project 1

Add a new system call void linux_survey_TT(char *) to your Linux kernel so that you can call it in your program

  1. The system call has a parameter which specifies the address of a memory area that can store all information the system call collects in the kernel.
  2. The system call records the virtual address intervals consisting of the user address space of the process executing the system call.
  3. The system call records the corresponding physical address intervals used by the above virtual address intervals at the moment that you execute system call void linux_survey_TT().

1. 第一個任務我們要取得呼叫此system call的process的virtual address intervals,而我們的目標就是去抓current裡面的mm_struct物件

Linux kernel 中進程用 task_struct 結構體表示

進程主要由以下幾部分組成:

  • 代碼段:編譯後形成的一些指令
  • 數據段:程序運行時需要的數據
    • 只讀數據段:常量
    • 已初始化數據段:全局變量,靜態變量
    • 未初始化數據段(bss):未初始化的全局變量和靜態變量
  • 堆棧段:程序運行時動態分配的一些內存
  • PCB:進程信息,狀態標識等

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

struct task_struct {
   volatile long state; //进程状态
   struct mm_struct *mm, *active_mm; //内存地址空间
   pid_t pid;
     pid_t tgid;

   struct task_struct __rcu *real_parent; //真正的父进程,fork时记录的
   struct task_struct __rcu *parent; // ptrace后,设置为trace当前进程的进程
   struct list_head children;  //子进程
     struct list_head sibling;	//父进程的子进程,即兄弟进程
   struct task_struct *group_leader; //线程组的领头线程

   char comm[TASK_COMM_LEN];  //进程名,长度上限为16字符
   struct fs_struct *fs;  //文件系统信息
   struct files_struct *files; // 打开的文件

   struct signal_struct *signal;
   struct sighand_struct *sighand;
   struct sigpending pending;
   
   void *stack;    // 指向内核栈的指针
   ...
}    

而其中mm_struct長這樣

struct mm_struct {
    struct vm_area_struct * mmap;        /* list of VMAs */
    struct rb_root mm_rb;
    unsigned long mmap_base;        /* base of mmap area */
    unsigned long task_size;        /* size of task vm space */
    pgd_t * pgd;
    atomic_t mm_count;            /* How many references to "struct mm_struct" (users count as 1) */
    int map_count;                /* number of VMAs */

    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;

    struct file *exe_file;

    /* ... some code omitted ... */
};

其中有關virtual address intervals的資訊:

unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;

這些變數存的是process memory layout中個區塊的起始, 結束位址,例如text segments就是start_code ~ end_code

struct vm_area_struct * mmap;

mmap紀錄進程使用到的VMA們
其中vm_area_struct中比較重要的資料有

unsigned long vm_start:記錄此 VMA 區塊的開始位址
unsigned long vm_end:記錄此 VMA 區塊的結束位址
struct vm_area_struct *vm_next:指向下一個 VMA 區塊結構的指標

我猜所有VMA的位址就是process的virtual address intervals了吧!

void my_copy(char *result, unsigned long address, size_t length){
	if(copy_to_user(result, &address, length))
		printk("error while copy_to_user\n");
}

while(mmap->vm_next != NULL){
        my_copy(result, mmap->vm_start, length);
        result += length;
        my_copy(result, mmap->vm_end, length);
        result += length;
        mmap = mmap->vm_next;
}
my_copy(result, state_end, length);
result += length;
result += length;

這邊在做的事就是把每一個vma的vm_start和vm_end搬到result,搬過去之後再將result往後length(sizeof(unsigned long))個bytes
由於我是每個兩個address為一組,所以最後state_end我也保留兩個變數的位址

2. 再來我們要取得corresponding physical address intervals

理解linux paging就很好找了

#include <linux/kernel.h>
#include <linux/sched.h>
#include <asm/pgtable.h>

static unsigned long vaddr2paddr(unsigned long vaddr){
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *pte;
        unsigned long paddr=0, page_addr=0, page_offset=0;

        pgd = pgd_offset(current->mm, vaddr);
        if (pgd_none(*pgd)) {
            printk("not mapped in pgd\n");
            return -1;
        }
        pud = pud_offset(pgd, vaddr);
        if (pud_none(*pud)) {
            printk("not mapped in pud\n");
            return -1;
        }
        pmd = pmd_offset(pud, vaddr);
        if (pmd_none(*pmd)) {
            printk("not mapped in pmd\n");
            return -1;
        }
        pte = pte_offset_kernel(pmd, vaddr);
        if (pte_none(*pte)) {
            printk("not mapped in pte\n");
            return -1;
        }

        page_addr = pte_val(*pte) & PAGE_MASK;
        page_offset = vaddr & ~PAGE_MASK;
        paddr = page_addr | page_offset;
        return paddr;
}

透過pgd_offset, pud_offset, pmd_offset, pte_offset_kernel取得page table,再配合PAGE_MASK取得physical address,其中並不是每個virtual address都有分配到physical address,所以可能在取某級分頁時發生取不到的狀況,此時我們就可以判斷該virtual address沒有被分配到

mmap = mm->mmap;
        while(mmap->vm_next != NULL){	
                for(i=mmap->vm_start ; i<=mmap->vm_end ; i+=(~PAGE_MASK)+1){
                        unsigned long page_start = i, page_end = i+(~PAGE_MASK);
                        unsigned long frame_start = vaddr2paddr(page_start), frame_end = vaddr2paddr(page_end);
                        if(frame_start){
                                my_copy(result, page_start, length);
                                result += length;
                                my_copy(result, page_end, length);
                                result += length;
			        my_copy(result, frame_start, length);
                                result += length;
                                my_copy(result, frame_end, length);
                                result += length;
			}
                }
                my_copy(result, vma_end, length);
                result += length;
                result += length;
                result += length;
                result += length;
                mmap = mmap->vm_next;
        } 
	my_copy(result, state_end, length);
	result += length;
	result += length;
        result += length;
        result += length;

由於virtual address對到physical address是以page為單位,也就是說一塊page的起始位置有對應的physical address則代表整塊page都有,所以我就遞迴過每個vma中的每個page
這邊我是以每四個資料為一組(page頭尾+frame頭尾),雖然這麼多result+=length有點醜不過我懶得用其他方法了

3. 列出呼叫system call時有多少virtual address有對應的physical address(幾趴)

我這邊測試端按照剛剛的儲存方式抓出,

  1. virtual address intervals
    這時可以順便計算virtual address總數
int virtual_cnt = 0, physical_cnt = 0;
const int length = sizeof(unsigned long);
for(int i=0 ; ; i++) {
    unsigned long vm_start, vm_end;
    memcpy(&vm_start, result, length); next
    memcpy(&vm_end, result, length); next
    if(vm_start == STATE_END)
        break;
    virtual_cnt += (vm_end - vm_start);
    fprintf(pfile, "vma%d: 0x%lx ~ 0x%lx\n", i+1, vm_start, vm_end);
}
  1. physical address intervals
    順便算出physical address總數
fprintf(pfile, "physical address intervals: \n");
for(int i=0 ; ; ){
    unsigned long page_start, page_end, frame_start, frame_end;
    memcpy(&page_start, result, length); next
    memcpy(&page_end, result, length); next
    memcpy(&frame_start, result, length); next
    memcpy(&frame_end, result, length); next
    if(page_start == STATE_END)
        break;
    if(page_start == VMA_END){
        // fprintf(pfile, "\n");
        continue;
    }
    physical_cnt += MASK;
    i++;
    fprintf(pfile, "0x%lx ~ 0x%lx -> 0x%lx ~ 0x%lx\n", page_start, page_end, frame_start, frame_end);
}	

其中我這邊把result+=length替換成next,還是沒多好看qq

  1. 印出
printf("%s virtual addresses that have physical memory: %.2f% \n", filename, (double)physical_cnt/virtual_cnt*100);

4. 印出哪些virtual address intervals對應到相同的physical address intervals

這裡就有點麻煩了,由於fork()後變成兩個process不能直接把資料存到變數中(child存的parent看不到),所以必須要再讀檔案來分析,超麻煩啊啊啊啊啊啊啊

  1. 先宣告一個struct來存我們要的資料
struct page{
	unsigned long page_start;
	unsigned long frame_start;
};
  1. 讀檔後存在陣列中
void analyze_file(char *filename, struct page *pages){
	FILE *pfile;
	pfile = fopen(filename, "r");
	if(!pfile){
		printf("open file error!\n");
		return;
	}
    // 先抓掉前面的vma list
	char useless[100];
	while(fscanf(pfile, "%[^\n]", useless) != EOF && strcmp(useless, "physical address intervals: "))
		fscanf(pfile, "\n", useless);
	fscanf(pfile, "%[^\n]", useless);
	fscanf(pfile, "\n", useless);
    // 我們要的資訊
	unsigned long page_start, page_end, frame_start, frame_end;
	int i=0;
	while(fscanf(pfile, "0x%lx ~ 0x%lx -> 0x%lx ~ 0x%lx\n", &page_start, &page_end, &frame_start, &frame_end) != EOF){
		pages[i].page_start = page_start;
		pages[i].frame_start = frame_start;
		i++;
	}

	fclose(pfile);
}
  1. 最後再分別拿兩個child的result做比對(我就直接暴搜了)
void calc_phy_relation(struct page *child, struct page *parent, int num){
	printf("\n\nthe virtual address intervals that map to same physical address at result_%d\n", num);
	for(int i=0 ; i<300 ; i++){
		if(!child[i].page_start && !child[i].frame_start) break;
		for(int j=0 ; j<300 ; j++){
			if(!parent[j].page_start && !parent[j].frame_start) break;
			if(child[i].frame_start == parent[j].frame_start){
				printf("0x%lx ~ 0x%lx\n", child[i].page_start, child[i].page_start+MASK-1);
				break;
			}
		}
	}
}

完成拉~~其實麻煩的都在資料處理qq

reference:
http://gityuan.com/2017/07/30/linux-process/
https://www.cnblogs.com/kwingmei/p/3731746.html
https://www.cnblogs.com/Rofael/archive/2013/04/13/3019153.html
https://www.ffutop.com/posts/2019-07-17-understand-kernel-13/
https://stackoverflow.com/questions/5748492/is-there-any-api-for-determining-the-physical-address-from-virtual-address-in-li
http://www.jollen.org/blog/2007/01/process_vma.html

Project 1問題

轉出的physical address有的會超大(前面多一串80000000),於是我把pgd_val, pud_val, pmd_val, pte_val都印出來發現只有pte_val前面多這一串,於是就先去看了下pte_offset_kernel

/*
 * the pte page can be thought of an array like this: pte_t[PTRS_PER_PTE]
 *
 * this function returns the index of the entry in the pte page which would
 * control the given virtual address
 */
static inline unsigned long pte_index(unsigned long address)
{
	return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}

static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
	return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}

發現很正常,和上幾層做的事都一樣,那看看pte_val

#define pte_val(x)	native_pte_val(x)
static inline pteval_t native_pte_val(pte_t pte)
{
	return pte.pte;
}

static inline pteval_t pte_flags(pte_t pte)
{
	return native_pte_val(pte) & PTE_FLAGS_MASK;
}

而當我去找native_pte_val時發現下面有一個function pte_flags,他從native_pte_val中取出flag的部分,也難怪直接取pte_val會包含flag,我們需要把flag去掉,所以先去看看PTE_FLAGS_MASK

/* Extracts the PFN from a (pte|pmd|pud|pgd)val_t of a 4KB page */
#define PTE_PFN_MASK		((pteval_t)PHYSICAL_PAGE_MASK)

/* Extracts the flags from a (pte|pmd|pud|pgd)val_t of a 4KB page */
#define PTE_FLAGS_MASK		(~PTE_PFN_MASK)

看到這邊就恍然大悟,因為我們最後取物理位址是用

pte_val(*pte) & PAGE_MASK + vaddr & ~PAGE_MASK

但是看看上面幾層在取得物理位址時都是用

pxx_page_vaddr(*pxx) + index

其中pxx_page_vaddr都是在做

pxx_val(*pxx) & PTE_PFN_MASK

我們將pte_val的flag濾掉就好了~

pte_val(*pte) & PTE_PFN_MASK & PAGE_MASK + vaddr & ~PAGE_MASK

page frame number

Virtual Memory Areas(VMA)

struct mm_struct 中有一個稱為 mmap 的 field,mmap 的 data type 為 struct vm_area_struct
Linux 以 struct vm_area_struct 資料結構來紀錄每一「區塊」的 VMA 資訊

VMA 是 user process 裡一段 virtual address space 區塊,virtual address space 是連續的記憶體空間,當然 VMA 也會是連續的空間。VMA 對 Linux 的主要好處是,可以記憶體的使用更有效率,並且更容易管理 user process address space。

從另一個觀念來看,VMA 可以讓 Linux kernel 以 process 的角度來管理 virtual address space。Process 的 VMA 對映,可以由 /proc/<pid>/maps 檔案查詢

src: http://www.jollen.org/blog/2007/01/linux_virtual_memory_areas_vma.html

struct vm_area_struct {
        struct mm_struct * vm_mm;
        unsigned long vm_start;
        unsigned long vm_end;

        struct vm_area_struct *vm_next;

        pgprot_t vm_page_prot;
        unsigned long vm_flags;

        rb_node_t vm_rb;

        struct vm_area_struct *vm_next_share;
        struct vm_area_struct **vm_pprev_share;

        struct vm_operations_struct * vm_ops;

        unsigned long vm_pgoff;

        struct file * vm_file;
        unsigned long vm_raend;
        void * vm_private_data;
};

Linux Paging

Linux中採用了一種通用的四級分頁機制

  • Page Global Directory 資料結構為 pgd_t
  • Page Upper Directory pmd_t
  • Page Middle Directory pud_t
  • Page Table pte_t

在這種分頁機制下,一個完整的線性地址被分為五部分:頁全局目錄+頁上級目錄+頁中間目錄+頁表+偏移量

不管系統採用多少級分頁模型,線性地址本質上都是索引+偏移量的形式,甚至你可以將整個線性地址看作N+1個索引的組合,N是系統採用的分頁級數。在四級分頁模型下,線性地址被分為5部分

src: http://edsionte.com/techblog/archives/3435

虛擬地址轉物理 :

  1. 從 CR3 register 中讀取 pgd 所在物理地址的基址,從 linear address 的第一部分獲取 pgd 的索引,兩者相加即是 virtual address 在 pgd 中對應欄的線性地址
arch/x86/include/asm/pgtable_64_types.h

/*
 * PGDIR_SHIFT determines what a top-level page table entry can map
 */
#define PGDIR_SHIFT     39
#define PTRS_PER_PGD    512
arch/x86/include/asm/pgtable.h

#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))

其中PGDIR_SHIFT 39 是 pmd(9)+pud(9)+pte(9)+offset(12)

PGD,PUD,PMD,PTE分別都是一個4k的page,PGD,PUD,PMD,PTE是四張table,table的大小都是4k,其中table的entry分別是: pgd_t, pud_t, pmd_t, pte_t,都是unsigned long類型(8個字節),4k(2的12次方)/8字節(2的3次方)= 512個entry(2的9次方)

  1. pgd_val(pgd) 取得 pgd 中那欄所指到的值,它與 PTE_PFN_MASK 計算後再丟到 __va() 即得到 pud 所在物理地址的基址,從 linear address 的第一部分獲取 pud 的索引,兩者相加即是 virtual address 在 pud 中對應欄的線性地址
arch/x86/include/asm/pgtable_64_types.h

/*
 * 3rd level page
 */
#define PUD_SHIFT       30
#define PTRS_PER_PUD    512
arch/x86/include/asm/pgtable_types.h

/* Extracts the PFN from a (pte|pmd|pud|pgd)val_t of a 4KB page */
#define PTE_PFN_MASK            ((pteval_t)PHYSICAL_PAGE_MASK)
arch/x86/include/asm/pgtable.h

static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
        return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}


/* to find an entry in a page-table-directory. */
static inline unsigned long pud_index(unsigned long address)
{
        return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}

static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
        return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}  
arch/x86/include/asm/page.h

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

後面步驟都差不多,最終拿到 pte 中對應欄的線性地址再用

page_addr = pte_val(*pte) & PTE_PFN_MASK & PAGE_MASK;
page_offset = vaddr & ~PAGE_MASK;
paddr = page_addr | page_offset;

copy-on-write

當我們fork()時,會產生一個和父進程完全相同的子進程(除了pid)
如果按傳統的做法,會直接將父進程的數據拷貝到子進程中,拷貝完之後,父進程和子進程之間的數據段堆棧是相互獨立的
但是,通常子進程都會執行exec()來做自己想要實現的功能。

exec()
exec函數的作用就是:裝載一個新的程序(可執行映像)覆蓋當前進程內存空間中的映像,從而執行不同的任務。
exec系列函數在執行時會直接替換掉當前進程的地址空間。

所以,如果按照傳統做法的話,創建子進程時復製過去的數據是沒用的(因為子進程執行exec(),原有的數據會被清空),既然很多時候複製給子進程的數據是無效的,於是就有了Copy On Write這項技術了

  • fork創建出的子進程,與父進程共享內存空間。也就是說,如果子進程不對內存空間進行寫入操作的話,內存空間中的數據並不會復制給子進程
  • 並且如果在fork函數返回之後,子進程第一時間 exec一個新的可執行映像,那麼也不會浪費時間和內存空間

結論:

  1. 在fork之後exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個
  2. 當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間
  3. 如果不是因為exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。
  4. 而如果是因為exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。

實測後:(唯一差別是virtual address都一樣,沒有像上面說的都會不同)

if not write:
    phyiscal address same
else
    if no exec:
        data, stack, heap segment different
        code segment same
    if exec:
        all different

http://eastrivervillage.com/The-Linux-COW/

asmlinkage | fastcall

linux支持多種CPU架構,比如x86、ppc和arm等,在不同的處理器結構上參數的傳遞方式都不同,例如

  • x86的函數參數和函數內部局部變量會被分配到函數的stack中
  • arm則是對函數調用過程中的傳參定義了一套規則,即ATPCS,規則中明確指出ARM中R0-R4都是作為通用寄存器使用,在函數調用時處理器從R0-R4中獲取參數,在函數返回時再將需要返回的參數一次存到R0-R4中,也就是說可以將函數參數直接存放在寄存器中

所以為了嚴格區別函數參數的存放位置,引入了兩個標記

  • asmlinkage 表示將函數參數存放在stack中
  • FASTCALL 是通知編譯器將函數參數用寄存器保存起來
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
#define FASTCALL(x)	x __attribute__((regparm(3)))
#define fastcall	__attribute__((regparm(3)))

attribute((regparm(0))):告訴gcc編譯器該函數不需要通過任何寄存器來傳遞參數,參數只是通過堆棧來傳遞。
attribute((regparm(3))):告訴gcc編譯器這個函數可以通過寄存器傳遞多達3個的參數,這3個寄存器依次為EAX、EDX和ECX。更多的參數才通過堆棧傳遞。這樣可以減少一些入棧出棧操作,因此調用比較快。

modules

  • buildin module
obj-y += filename.o

編譯的時候就會在對應目錄產生built-in.o的檔案
這些檔案便會被包入在image裡面

  • externel module
obj-m += filename.o

編譯的時候就會在對應目錄產生kernel/time/test_udelay.ko的檔案
這些檔案便會被放置在/lib/modules/$(uname -r)

http://yi-jyun.blogspot.com/2017/07/linux-kernel-modules.html

直接訪問實體記憶體位址

busybox 中的 devmem 可直接操作物理地址
https://github.com/brgl/busybox/blob/master/miscutils/devmem.c

其中他使用到 mmap 和 /dev/mem,透過 mmap 將 /dev/mem 的物理地址映射到 user space,我們就可以像操作虛擬地址般讀寫

Usage: devmem ADDRESS [WIDTH [VALUE]]

Read/write from physical address

        ADDRESS Address to act upon
        WIDTH   Width (8/16/...)
        VALUE   Data to be written

要實作出 devmem 有三個主要步驟

  1. 開啟 /dev/mem
fd = open("/dev/mem", argv[3] ? (O_RDWR | O_SYNC) : (O_RDONLY | O_SYNC));
  1. 將物理地址透過 mmap 映射到 user space
 /* void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); */
map_base = mmap(0, MAP_SIZE, argv[3] ? (PROT_READ | PROT_WRITE) : PROT_READ, MAP_SHARED, fd, target & MAP_MASK);
  1. 算出地址後進行讀寫
virt_addr = map_base + (target & MAP_MASK);
// write
*(*type)virt_addr = write_val
// read
read_val = *(*type)virt_addr

觀察 mmap

static int mmap_mem(struct file *file, struct vm_area_struct *vma)
{
	size_t size = vma->vm_end - vma->vm_start;
	phys_addr_t offset = (phys_addr_t)vma->vm_pgoff << PAGE_SHIFT;

	/* It's illegal to wrap around the end of the physical address space. */
	if (offset + (phys_addr_t)size - 1 < offset)
		return -EINVAL;

	if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))
		return -EINVAL;

	if (!private_mapping_ok(vma))
		return -ENOSYS;

	if (!range_is_allowed(vma->vm_pgoff, size))
		return -EPERM;

	if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,
						&vma->vm_page_prot))
		return -EINVAL;

	vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
						 size,
						 vma->vm_page_prot);

	vma->vm_ops = &mmap_mem_ops;

	/* Remap-pfn-range will mark the range VM_IO */
	if (remap_pfn_range(vma,
			    vma->vm_start,
			    vma->vm_pgoff,
			    size,
			    vma->vm_page_prot)) {
		return -EAGAIN;
	}
	return 0;
}
  1. valid_mmap_phys_addr_range 檢查物理地址是不是在範圍內
/*
 * Do not allow /dev/mem mappings beyond the supported physical range.
 */
int valid_mmap_phys_addr_range(unsigned long pfn, size_t size)
{
	return (pfn + (size >> PAGE_SHIFT)) <= (1 + (PHYS_MASK >> PAGE_SHIFT));
}
  1. private_mapping_ok 對於有 MMU 的 CPU 直接回傳 1(MMU的權限管理可以支持私有映射)
#ifndef CONFIG_MMU
...
#else

static inline int private_mapping_ok(struct vm_area_struct *vma)
{
	return 1;
}
#endif
  1. range_is_allowed 以 frame 為單位檢查物理地址,每一頁都呼叫devmem_is_allowed 檢查
#ifdef CONFIG_STRICT_DEVMEM
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
	u64 from = ((u64)pfn) << PAGE_SHIFT;
	u64 to = from + size;
	u64 cursor = from;

	while (cursor < to) {
		if (!devmem_is_allowed(pfn)) {
			printk(KERN_INFO
		"Program %s tried to access /dev/mem between %Lx->%Lx.\n",
				current->comm, from, to);
			return 0;
		}
		cursor += PAGE_SIZE;
		pfn++;
	}
	return 1;
}
#else
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
	return 1;
}
#endif

3.1 devmem_is_allowed

/*
 * devmem_is_allowed() checks to see if /dev/mem access to a certain
 * address is valid. The argument is a physical page number.
 * We mimic x86 here by disallowing access to system RAM as well as
 * device-exclusive MMIO regions. This effectively disable read()/write()
 * on /dev/mem.
 */
int devmem_is_allowed(unsigned long pfn)
{
	if (iomem_is_exclusive(pfn << PAGE_SHIFT))
		return 0;
	if (!page_is_ram(pfn))
		return 1;
	return 0;
}

3.1.1 iomem_is_exclusive 遍歷 iomem_resource,檢查物理地址是否為 busy 或 exclusive

#ifdef CONFIG_STRICT_DEVMEM
static int strict_iomem_checks = 1;
#else
static int strict_iomem_checks;
#endif

/*
 * check if an address is reserved in the iomem resource tree
 * returns 1 if reserved, 0 if not reserved.
 */
int iomem_is_exclusive(u64 addr)
{
	struct resource *p = &iomem_resource;
	int err = 0;
	loff_t l;
	int size = PAGE_SIZE;

	if (!strict_iomem_checks)
		return 0;

	addr = addr & PAGE_MASK;

	read_lock(&resource_lock);
	for (p = p->child; p ; p = r_next(NULL, p, &l)) {
		/*
		 * We can probably skip the resources without
		 * IORESOURCE_IO attribute?
		 */
		if (p->start >= addr + size)
			break;
		if (p->end < addr)
			continue;
		if (p->flags & IORESOURCE_BUSY &&
		     p->flags & IORESOURCE_EXCLUSIVE) {
			err = 1;
			break;
		}
	}
	read_unlock(&resource_lock);

	return err;
}

對於外設的IO資源,kernel中使用platform device機制來註冊平台設備(platform_device_register)時調用insert_resource將該設備相應的io資源插入到iomem_resource鍊錶中。
如果我要對某外設的IO資源進行保護,防止用戶空間訪問,可以將其resource的flags置位exclusive即可。
https://blog.csdn.net/skyflying2012/article/details/47611399

3.1.2 不允許訪問 ram 地址

int page_is_ram(unsigned long pfn)
{
#ifndef CONFIG_PPC64	/* XXX for now */
	return pfn < max_pfn;
#else
	unsigned long paddr = (pfn << PAGE_SHIFT);
	struct memblock_region *reg;

	for_each_memblock(memory, reg)
		if (paddr >= reg->base && paddr < (reg->base + reg->size))
			return 1;
	return 0;
#endif
}
  1. phys_mem_access_prot_allowed 直接回傳1
  2. phys_mem_access_prot 確定我們映射頁的權限
  3. 最後呼叫 remap_pfn_range 設定 paging

結論: 如果有開 CONFIG_STRICT_DEVMEM 會先檢查

  1. 物理地址不能超過上限
  2. 不能是exclusive
  3. 不能是ram

關閉的話就沒有限制

Project 2

Write a new system call get_process_zero_session_group(unsigned int *, int) so that a process can use it to get the global PIDs of all processes that are in the same login session of process 0

  1. 找到 process 0 的 sid
  2. 遍歷所有 process 比對 sid

Linux PID

在linux中ID主要有以下四種:

  1. PID
  2. TGID
    如果一個行程是以CLONE_THREAD建立,則它處於一個Thread Groupˋˋ,ID就是TGID,相同的Thread Group TGID都相同,其中Thread Group leader或是沒有使用執行續的PID和TGID相同。
  3. PGID
    行程可以組成行程組,PGID等於組長ID
  4. SID
    行程組也可以組成session

為了管理PID linux使用了許多數據結構,直接從原始碼觀察比較難理解,以下我們慢慢將需求加入來分析

忽略行程之間的關係及namespace

struct task_struct {
    ...
    struct pid_link pids;
    ...
};

struct pid_link {
    struct hlist_node node;
    struct pid *pid;
};

struct pid {
    struct hlist_head tasks;
    int nr;  // PID
    struct hlist_node pid_chain;
};

每個 task_struct 有個指向 struct pid 的指標,strcut pid 包含 PID

  • pid_hash
  • pid_map

考慮行程間的關係

task_struct 中的 pid_link 多加幾項來只到其組長的 struct pid
struct pid 加上幾項來指回組長的 task_struct

enum pid_type {
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX // number of ID type (not include TGID)
};

struct task_struct {
    ...
    pid_t pid;
    pid_t tgid;
    struct task_struct *group_leader; // Thread group leader
    struct pid_link pids[PIDTYPE_MAX];
    ...
};

struct pid_link {
    struct hlist_node node;
    struct pid *pid;
};

struct pid {
    struct hlist_head tasks[PIDTYPE_MAX];
    int nr;
    int hlist_node pid_chain;
}

假設今天有三個行程A B C在同一個行程組,其中A是行程組組長

  • B, C 的 pids[PIDTYPE_PFID] 指向 A 的 struct pid
  • A 的 struct pid 的 task[PIDTYPE_PGID] 串連起所有以該 PID 為組長的的行程

增加 PID Namespace

在每個可見行程的 namespace 都會給該行程分配一個 PID,所以一個行程可能有多個 PID

struct pid{
    unsigned int level;
    struct hlist_head tasks[PIDTYPE_MAX];
    struct upid numbers[1];
}

struct upid{
    int nr;
    struct pid_namespace *ns;   // pid 所處 namespace
    struct hlist_node pid_chain;
}

numbers[0] 表示 global namespace,numbers[i] 表示第 i 層 namespace,i 越大所在層級越低

原碼

include/linux/sched.h

struct task_struct {
	···
	pid_t                 pid;
	pid_t                 tgid;
	struct task_struct   *group_leader;
	struct pid_link       pids[PIDTYPE_MAX];
	struct nsproxy       *nsproxy;
	···
};

其中 nsproxy 存該行程相關 namespace 資訊

include/linux/nsproxy.h

struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children;
    struct net 	     *net_ns;
    struct cgroup_namespace *cgroup_ns;
};
include/linux/pid.h

struct upid {
        /* Try to keep pid_chain in the same cacheline as nr for find_vpid */
        int nr;
        struct pid_namespace *ns;
        struct hlist_node pid_chain;
};

struct pid
{
	atomic_t count;
	unsigned int level;
	/* lists of tasks that use this pid */
	struct hlist_head tasks[PIDTYPE_MAX];
	struct rcu_head rcu;
	struct upid numbers[1];
};

reference:
https://www.cnblogs.com/hazir/p/linux_kernel_pid.html
https://carecraft.github.io/basictheory/2017/03/linux-pid-manage/

tags: class