Linux でのアドレス変換

1. コードを実行してスクリーンショットを撮ります。

paging_lowmem.c コードは次のとおりです。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/mm_types.h>
#include <linux/sched.h>
#include <linux/export.h>
#include <linux/delay.h>


static unsigned long cr0,cr3;

static unsigned long vaddr = 0;


static void get_pgtable_macro(void)  /*打印页机制中的一些重要参数*/
{
    cr0 = read_cr0();
    cr3 = read_cr3_pa();
     
    printk("cr0 = 0x%lx, cr3 = 0x%lx\n",cr0,cr3);
    
    /*这些宏是用来指示线性地址中相应字段所能映射的区域大小的对数的*/
    printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);  
    printk("P4D_SHIFT = %d\n",P4D_SHIFT);
    printk("PUD_SHIFT = %d\n", PUD_SHIFT);
    printk("PMD_SHIFT = %d\n", PMD_SHIFT);
    printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);   /*指示page offset字段,映射的是一个页面的大小,一个页面大小是4k,转换成以2为底的对数就是12,其他的宏类似*/
 
 /*下面的这些宏是用来指示相应的页目录表中的项的个数的,这些宏都是为了方便寻页时进行位运算的*/
    printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
    printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
    printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
    printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
    printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
    printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);   /*page_mask,页内偏移掩码,用来屏蔽掉page offset字段*/
}
 
static unsigned long vaddr2paddr(unsigned long vaddr)  /*线性地址到物理地址转换*/
{
    /*首先为每个目录项创建一个变量将它们保存起来*/
    pgd_t *pgd;
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    
    unsigned long paddr = 0;
    unsigned long page_addr = 0;
    unsigned long page_offset = 0;
    
    pgd = pgd_offset(current->mm,vaddr);  /*第一个参数是当前进程的mm_struct结构(我们申请的线性地址空间是内核,所以应该查内核页表,又因为所有的进程都共享同一个内核页表,所以可以用当前进程的mm_struct结构来进行查找),pgd为页全局目录项*/
    printk("pgd_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd),pgd_index(vaddr));
    if (pgd_none(*pgd)){
        printk("not mapped in pgd\n");
        return -1;
    }

    p4d = p4d_offset(pgd, vaddr);  /*查找到的页全局目录项pgd作为下级查找的参数传入到p4d_offset中*/
    printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d),p4d_index(vaddr));
    if(p4d_none(*p4d))
    { 
        printk("not mapped in p4d\n");
        return -1;
    }

    pud = pud_offset(p4d, vaddr);
    printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud),pud_index(vaddr));
    if (pud_none(*pud)) {
        printk("not mapped in pud\n");
        return -1;
    }
 
    pmd = pmd_offset(pud, vaddr);
    printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd),pmd_index(vaddr));
    if (pmd_none(*pmd)) {
        printk("not mapped in pmd\n");
        return -1;
    }
 
    pte = pte_offset_kernel(pmd, vaddr);  /*与上面略有不同,这里表示在内核页表中查找,而在进程页表中查找是另外一个完全不同的函数   这里最后取得了页表项的物理地址*/
    printk("pte_val = 0x%lx, ptd_index = %lu\n", pte_val(*pte),pte_index(vaddr));

    if (pte_none(*pte)) {
        printk("not mapped in pte\n");
        return -1;
    }

    page_addr = pte_val(*pte) & PAGE_MASK;    /*取出其高52位*/
    /*取出页偏移地址,页偏移量也就是线性地址中的低12位*/
    page_offset = vaddr & ~PAGE_MASK;
    /*将两个地址拼接起来,就得到了想要的物理地址了*/
    paddr = page_addr | page_offset;
    printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
    printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
    return paddr;
}

static int __init v2p_init(void)    /*内核模块的注册函数*/
{
    unsigned long vaddr = 0 ;
    printk("vaddr to paddr module is running..\n");
    get_pgtable_macro();
    printk("\n");
    vaddr = __get_free_page(GFP_KERNEL);   /*在内核的ZONE_NORMAL中申请了一块页面,GFP_KERNEL标志指示优先从内核的ZONE_NORMAL中申请页框*/
    if (vaddr == 0) {
        printk("__get_free_page failed..\n");
        return 0;
    }
    sprintf((char *)vaddr, "hello world from kernel");   /*在地址中写入hello...*/
    printk("get_page_vaddr=0x%lx\n", vaddr);
    vaddr2paddr(vaddr);
    ssleep(600);
    return 0;
}
static void __exit v2p_exit(void)    /*内核模块的卸载函数*/
{
    printk("vaddr to paddr module is leaving..\n");
    free_page(vaddr);   /*将申请的线性地址空间释放掉*/
}


module_init(v2p_init);
module_exit(v2p_exit);
MODULE_LICENSE("GPL"); 

Makefile コードは次のとおりです。

obj-m:= paging_lowmem.o

CURRENT_PATH:=$(shell pwd)	#模块所在的当前所在路径
LINUX_KERNEL:=$(shell uname -r)	#linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)	#linux内核的当前版本源码路径

all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules	#编译模块
#				内核的路径		  当前目录编译完放哪   表明编译的是内核模块

clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean	#清理模块

操作結果:

ここに画像の説明を挿入します

PGD​​IR_SHIFT と P4D_SHIFT が両方とも 39 であることがわかります。これは、リニア アドレスでは P4D フィールドが空であることを意味します。また、ページ ディレクトリ エントリでは、P4D のページ ディレクトリ エントリも 1 であることがわかります。 Linux は 5 レベルのページ テーブル モデルを採用していますが、実際には 4 つのページ テーブルしか使用されていないことを示しています。

PAGE_MASK は、下位 12 ビットがすべて 0、残りのビットがすべて 1 である 64 ビットの数値です。

これは、物理ページ フレームがコードの実行に使用できないことを識別するために X86 プラットフォームで使用される保護ビットです。ページフレームの物理アドレスは次の9ビットです

2. デバッグツールを使用してデバッグする

デバッグツール:

① dram カーネルモジュールの主な機能は、物理メモリ上のデータを mmap を介してデバイス ファイルにマッピングすることであり、このデバイス ファイルにアクセスすることで、物理メモリにアクセスする機能を実現できます。

②ファイルビュー、このバイナリファイルを希望の形式で読み込むことができます

これら 2 つのツールのソース コードについては、http://t.csdn.cn/2lc7y を参照してください。

補足: EXPORT_SYMBOL の役割: EXPORT_SYMBOL タグで定義された関数またはシンボルは、すべてのカーネル コードに公開されており、カーネル コードを変更せずにカーネル モジュール内で直接呼び出すことができます。それの使い方:

1.モジュール関数定義の後に「EXPORT_SYMBOL(関数名)」を使用して宣言します。

2. extern を使用して、関数を呼び出す別のモジュールで宣言します。

3. 最初に関数を定義するモジュールをロードし、次に関数を呼び出すモジュールをロードします。この順序に注意してください。

別のターミナル ウィンドウを開き、次のコマンドを順番に実行します。

sudo insmod dram.ko

sudo mknod /dev/dram c 85 0

./fileview /dev/dram

操作結果:

ここに画像の説明を挿入します

アドレス指定プロセス:

ページディレクトリ バイナリ 10進数*8B 16進数*8B
PGD 1 0011 0111(39) 311*8B 9b8
PUD 0 1100 0101(30) 197*8B 628
PMD 0 1000 1011(21) 139*8B 458
PTE 0 0001 1000(12) 24*8B c0

ページのグローバル ディレクトリ テーブルのベース アドレス cr3 = 0x118fa000

0x118fa000+0x9b8 = 0x118fa9b8 → 3a202067、これは pgd_val 値 (下位レベルのページテーブルの物理アドレス)

0x3a202000+0x628 = 0x3a202628 → 3a203067 は pud_val 値 (下位レベルのページ テーブルの物理アドレス)

0x3a203000 + 0x458 =0x3a203458 → 12148063、これは pmd_val 値 (下位レベルのページ テーブルの物理アドレス)

0x12148000 + 0xc0 = 0x121480c0 → 8000000011618063 は pte_val 値 (ページの物理アドレス)

ページ オフセット page_offset は 0 です

このことから、必要な物理アドレスは 11618000 であることが推測できます。

ここに画像の説明を挿入します

3. 述べた原則に基づいて分析し、自分の意見を持つ

セグメント機構:仮想アドレス→リニアアドレス

Intel の 80x86 プロセッサにおける仮想アドレスからリニア アドレスへの変換については、「Linux オペレーティング システムの原理と応用」P29 を参照してください。

ここに画像の説明を挿入します

Linux カーネルの設計では、Intel が提供するセグメンテーション ソリューションをすべて使用するわけではなく、限られた範囲でのみセグメンテーション メカニズムを使用するため、Linux カーネルの設計が簡素化されるだけでなく、Linux を他のプラットフォームに移植するための条件も作成されます。Linuxの設計者は、セグメントのベースアドレスを0、セグメントリミットを4GBに設定していますが、このときオフセットを任意に与えると、「0 + オフセット = リニアアドレス」、つまり「オフセットアドレス」となります。 「 = リニアアドレス」。

ページング機構の起源

ページングが使用されず、リニア アドレス空間が物理空間に直接マッピングされている場合、任意のセグメントのデータを変更すると、同時に他のセグメントのデータも変更されることが想像できます。変更しない場合、セグメント メカニズムはは、セグメントが互いに完全に分離されるように線形アドレス空間を分割する「ベース アドレス: リミット」メソッドを提供します。セグメント保護を実装するこの方法は、セグメントが相互に上書きされる可能性があるため、単に機能しません。

ページングメカニズム: リニアアドレス → 物理アドレス

32 ビットのリニア アドレスから 2 レベルのページ テーブルの物理アドレスへの変換については、「Linux オペレーティング システムの原理と応用」P34 を参照してください。

ここに画像の説明を挿入します

現在、32 ビットおよび 64 ビットの CPU と互換性を持たせるために、Linux は統一されたページ アドレス モデルを必要としています。最も一般的に使用されているのは、4 レベルのページ テーブル モデルです。

ここに画像の説明を挿入します

64 ビット プロセッサであるため64 ビット Linux では、4 レベルのページ テーブル構造が使用されており、そのリニア アドレス分割は次の図に示されています。この場合、ページ サイズは 4kb、各ページ テーブル エントリは 8 ビットで、ページ テーブル全体が256TBのスペースをマップします。新しい Intel チップの MMU ハードウェアは 5 レベルのページ テーブル管理を提供するため、4.15 カーネルでは、Linux はページ グローバル ディレクトリとページ上位ディレクトリの間に、P4D ページ ディレクトリ (PGD と PUD の間) と呼ばれる新しいページ ディレクトリを追加します。 )。CR3 レジスタは、現在のプロセスのページ グローバル ディレクトリのアドレスを保存するために使用され、ページングの開始はページ グローバル ディレクトリから始まります。

ここに画像の説明を挿入します

64 ビットのリニア アドレスから 5 レベルのページ テーブルの物理アドレスへの変換は、2 レベルのページ テーブルの 32 ビットのリニア アドレスから物理アドレスへの変換を指します。最初の質問の c コード:

vaddr はリニアアドレスです

	pgd = pgd_offset(current->mm, vaddr);

最初のパラメータは、現在のプロセスの mm_struct 構造体です。mm_struct 構造体は、プロセスの仮想アドレス空間を記述するために使用されます。mm_struct には、プロセスのページ グローバル ディレクトリの物理アドレスを保存するために使用されるフィールド PGD があります。 。このコード行は、PGD テーブル エントリの物理アドレス pgd を検索し、この物理アドレスの下に下位レベルのページ テーブルの物理アドレスが格納されます。

	p4d = p4d_offset(pgd, vaddr);

このコード行は、P4D テーブル エントリの物理アドレス p4d を検索します。ページの第 4 レベル ディレクトリが有効になっていないため、ディレクトリ テーブル エントリは 1、つまり p4d=pgd になります。

などなど...、最後に:

	pte = pte_offset_kernel(pmd, vaddr);

この時点で、PTE エントリの物理アドレス pte が取得され、この物理アドレスの下にページの物理アドレスが格納されます。これは、2 レベルの 32 ビット リニア アドレスから物理アドレスへの変換の 3 番目のステップに相当します。ページテーブル。

	page_addr = pte_val(*pte) & PAGE_MASK;    /*取出其高52位*/
	/*取出页偏移地址,页偏移量也就是线性地址中的低12位*/
	page_offset = vaddr & ~PAGE_MASK;
	/*将两个地址拼接起来,就得到了想要的物理地址了*/
	paddr = page_addr | page_offset;

最後のステップでは、上位ビット (64 ビットの場合は上位 52 ビット、32 ビットの場合は上位 20 ビット) を取得し、それをリニア アドレスの下位 12 ビット、つまりオフセットと連結します。結果は次のようになります。物理アドレス

4. 2 つの質問をし、答える

ページのグローバル ディレクトリのアドレスはどこですか?

カーネルがプロセスを作成すると、それにページ グローバル ディレクトリが割り当てられます。プロセス記述子の task_struct 構造体には、mm_struct 構造体を指すポインタ mm があり、mm_struct 構造体はプロセスの仮想アドレス空間を記述するために使用されます。 mm_struct にはフィールド PGD があり、プロセスのページ グローバル ディレクトリの物理アドレスを保存するために使用されます。(プロセスが切り替わると、オペレーティング システムは、task_struct 構造にアクセスし、次に mm_struct 構造にアクセスし、最後に PGD フィールドを見つけて、新しいプロセスのページ グローバル ディレクトリのアドレスを取得することによって、ページ テーブルの切り替えを完了します。そしてそれをCR3レジスタに記入します)

Linux では論理アドレスがリニア アドレスと等しいのはなぜですか?

すべての Linux セグメント (ユーザー コード セグメント、ユーザー データ セグメント、カーネル コード セグメント、カーネル データ セグメント) のリニア アドレスは 0x00000000 から始まり、長さが 4G であるため、このように、リニア アドレス = 論理アドレス + 0x00000000、つまり、論理アドレスはリニアアドレスと同じです。

おすすめ

転載: blog.csdn.net/qq_58538265/article/details/133920479