日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

head.s 分析——Linux-0.11 学习笔记(三)

發布時間:2025/3/15 linux 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 head.s 分析——Linux-0.11 学习笔记(三) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

題目:head.s 分析

head.s 程序在被編譯生成目標文件后會與內核其他程序一起被鏈接成 system 模塊,它位于 system 模塊的最開始部分,這也就是為什么稱其為“頭部(head)”程序的原因。

從這里開始,內核完全是在保護模式下運行了。head.s 匯編程序與前面的語法格式不同,它采用的是AT&T匯編語言格式,并且需要使用 GNU 的 as 和 ld 進行編譯和連接。因此請注意代碼中賦值的方向是從左到右

這段程序實際上處于內存地址0處,在理解代碼的時候,請務必記住。

一、加載段寄存器

.text .globl idt,gdt,pg_dir,tmp_floppy_area pg_dir: # 頁目錄將會存放在這里,把這里的代碼覆蓋掉 .globl startup_32 startup_32:movl $0x10,%eaxmov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss stack_start,%esp

第6行:0x10 是數據段的選擇子,在 setup.s 文件的末尾處定義,基地址是 0,段界限是 0x7FF,粒度 4KB,可讀可寫,向上擴展。如果讀者忘了,可以參考我的博文:setup.s 分析

7~10行:令ds,es,fs,gs指向數據段。

第11行:stack_start 的定義在 kernel/sched.c(以后會分析)中。

為了閱讀方便,截取部分代碼在這里。

// kernel/sched.c long user_stack [ PAGE_SIZE>>2 ] ;struct {long * a;short b;} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

LSS指令

lss指令的格式是

LSS r32,m16:32

含義是用內存中的長指針加載 SS:r32

Load SS:r32 with far pointer from memory

m16:32表示一個內存操作數,這個操作數是一個長指針,由2部分組成:16位的段選擇子和32位的偏移。

A memory operand containing a far pointer composed of two numbers. The number to the left of the colon corresponds to the pointer’s segment selector. The number to the right corresponds to its offset.

注意,長指針在內存中的布局如下:低4字節是偏移,高2字節是段選擇子。

stack_start 處的6字節是long * a和short b.
a被賦值為user_stack[]數組最末端的地址,b被賦值為0x10.

所以,第11行代碼表示用a的值加載ESP,用b的值加載SS,即棧的初始化。

二、設置中斷描述符表(IDT)

call setup_idt setup_idt:lea ignore_int,%edxmovl $0x00080000,%eaxmovw %dx,%ax /* selector = 0x0008 = cs */movw $0x8E00,%dx /* interrupt gate:dpl=0, present */lea idt,%edi # 取idt的偏移給edimov $256,%ecx # 循環256次 rp_sidt:movl %eax,(%edi) # eax -> [edi]movl %edx,4(%edi) # edx -> [edi+4]addl $8,%edi # edi + 8 -> edidec %ecxjne rp_sidtlidt idt_descr # 加載IDTRret...idt_descr:.word 256*8-1 # idt contains 256 entries.long idt # IDT 的線性基地址...idt: .fill 256,8,0 # idt is uninitialized

IDT 共 256 項,作者使各個表項均指向一個只報錯誤的啞中斷子程序ignore_int。

2~5行:組裝中斷門,示意圖如下,藍色圓圈是行號。

2~5行:在edx、eax中組合設置出8字節默認的中斷描述符值。eax 含有描述符低4字節,edx 含有高4字節。

9~14行:在idt表每一項中都放置該描述符,共 256 項。內核在隨后的初始化過程中會替換那些真正使用的中斷描述符項。

中斷處理過程 ignore_int

/* This is the default interrupt "handler" :-) */ int_msg:.asciz "Unknown interrupt\n\r" .align 2 ignore_int:pushl %eaxpushl %ecxpushl %edxpush %ds # 這里請注意ds,es,fs,gs等雖然是16位的寄存器,# 但仍然會以32位的形式入棧,即需要占用4個字節的棧空間。 push %espush %fs # 以上用于保存寄存器movl $0x10,%eax # 0x10是數據段選擇子mov %ax,%dsmov %ax,%esmov %ax,%fs # ds,es,fs均指向數據段pushl $int_msg call printk # 該函數在 kernel/printk.c 中popl %eax # 清理參數pop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret

第17行:把 printk 函數的參數入棧。注意:若符號 int_msg 前不加 $,則表示把 int_msg 符號處的雙字Unkn入棧。

第18行:調用 printk 函數,該函數在 kernel/printk.c 中,以后再具體分析。

第19行:清理參數 $int_msg.

說明:匯編程序調用C函數時,函數的入口參數使用棧來傳送,參數的傳遞順序是從右到左。調用者負責清除參數占用的棧空間。C函數的返回值如果是32位整數,則保存在eax寄存器;如果是64位整數,則保存在edx:eax寄存器。

具體可以參考我的博文: 在匯編程序中調用C函數

三、設置全局描述符表(GDT),加載 GDTR

call setup_gdt setup_gdt:lgdt gdt_descr # 加載GDTRret

LGDT指令的格式是:

LGDT m16&32

該指令的操作數是一個 48 位(6字節)的內存區域。在這6字節的內存區域中,要求前(低)16位是 GDT 的界限值,后(高)32 位是 GDT 的基地址 。該指令在實模式和保護模式下都可以執行。

gdt_descr:.word 256*8-1 .long gdt gdt: .quad 0x0000000000000000 /* NULL descriptor */.quad 0x00c09a0000000fff /* 16Mb */.quad 0x00c0920000000fff /* 16Mb */.quad 0x0000000000000000 /* TEMPORARY - don't use */.fill 252,8,0 /* space for LDT's and TSS's etc */

GDT 定義的描述符如下:

索引選擇子段類型LimitGDPL其他
0------
10x08代碼段0xFFF1(表示4KB)0非一致,可讀
20x10數據段0xFFF1(表示4KB)0向上擴展,可寫

段長度可以這樣算:(Limit + 1)* 4KB = (0xFFF + 1) * 4KB = 0x1000 * 4KB = 16MB

四、重新加載段寄存器

call setup_idt call setup_gdt movl $0x10,%eax # reload all the segment registers mov %ax,%ds # after changing gdt. CS was already mov %ax,%es # reloaded in 'setup_gdt' mov %ax,%fs mov %ax,%gs lss stack_start,%esp

由于段描述符中的段限長(Limit)從setup.s中的 8MB 改成了本程序設置的 16MB,因此這里再次對所有段寄存器執行加載操作是必須的。另外,如果不對 CS 再次加載,那么在執行到第1行時,CS段寄存器的“描述符高速緩存器”中的段限長還是 8MB。這樣看來應該重新加載CS

但是由于 setup.s 中的代碼段描述符與本程序中重新設置的代碼段描述符除了段限長以外其余部分完全一樣,8MB 的限長在內核初始化階段不會有問題,而且在以后內核執行段間跳轉時會重新加載 CS,因此這里沒有加載它并沒有讓程序出錯。

針對該問題,目前內核中就在movl $0x10,%eax之前添加了一條長跳轉指令ljmp $(_KERNEL_CS), $1f,大概的代碼如下:

call setup_idtcall setup_gdt # reload all the segment registersljmp $(_KERNEL_CS), $1f 1:movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fsmov %ax,%gslss stack_start,%esp

注意:以上的代碼只是為了說明問題,并非源碼。

ljmp $(_KERNEL_CS), $1f

$1f中的1是標號,緊跟在其后的f表示向前(forwards)。這條指令會跳轉到第6行來確保 CS 被重新加載。

五、檢測A20是否開啟

xorl %eax,%eax 1: incl %eax # check that A20 really is enabledmovl %eax,0x000000 # loop forever if it isn'tcmpl %eax,0x100000je 1b

用于測試 A20 地址線是否已開啟。采用的方法是向內存地址 0x0_0000 處寫入任意一個數值,然后看內存地址 Ox10_0000(1M)處是否也是這個數值。如果一直相同的話,就一直比較下去。死機表示 A20 線沒有選通。

六、檢測x87協處理器

為了彌補x86系列在進行浮點運算時的不足,Intel于1980年推出了x87系列數學協處理器,那時x87是一個外置的、可選的芯片。1989年,Intel發布了486處理器。從486開始,以后的CPU一般都內置了協處理器。這樣,對于486以前的計算機而言,操作系統檢測x87協處理器是否存在就非常必要了。

注:1991年,一名21歲的就讀于芬蘭赫爾辛基大學的計算機科學專業學生—— Linus Torvalds 基于 gcc、bash 開發了針對 386 機器的 Liniux內核。

下面這段程序用于檢查數學協處理器芯片是否存在。方法是修改控制寄存器CR0,在假設協處理器存在的情況下執行一個協處理器指令,如果出錯的話則說明協處理器芯片不存在。

movl %cr0,%eax # check math chipandl $0x80000011,%eax # Save PE ET PGorl $2,%eax # set MP=1movl %eax,%cr0call check_x87jmp after_page_tables

第2行:保留PE、ET、PG位,其他位都清零。

PE 指示是否開啟保護模式。PG 指示是否開啟分頁。

關于ET,Intel 手冊如是說:

Extension Type (bit 4 of CR0). Reserved in the P6 family and Pentium ? processors. (In the P6 family processors, this flag is hardcoded to 1.) In the Intel 386? and Intel 486? processors, this flag indicates support of Intel 387 DX math coprocessor instructions when set.

第3行,設置MP=1.

這塊我不是很明白,根據下面的表格,在數學協處理器存在的時候,推薦設置EM=0,MP=1.

既然作者的意圖是假設數學協處理器存在,那么就設置 EM=0,MP=1 吧。

check_x87:fninit # 向協處理器發出初始化命令fstsw %ax # 把FPU的狀態字保存到AX中# 初始化后狀態字應該為0,否則說明協處理器不存在cmpb $0,%alje 1f # 存在則跳轉到標號1處movl %cr0,%eax xorl $6,%eax # 把 eax 的值和 0110b 異或movl %eax,%cr0ret.align 2 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ret

第12行:0xDB,0xE4這兩個字節是 80287 協處理器指令 fsetpm 的機器碼。其作用是把 80287 設置為保護模式。80387 無需該指令,它會把該指令看作是空操作。

關于異或

按位異或的3個特點

  • 0異或任何數 = 任何數
  • 1異或任何數 = 任何數取反
  • 任何數異或自己 = 把自己置0
  • 按位異或的幾個常見用途

    1. 使某些特定的位翻轉

    ? 例如要使 EAX 的 b1 位和 b2 位翻轉:
          EAX = EAX ^ 00000110

    ? 代碼第8行就是這種用法,把 EM 和 MP 翻轉。

    2. 不使用臨時變量就可以實現兩個值的交換

    ? 例如 a=11110000,b=00001111,要交換a、b的值,可通過下列語句實現:

    a = a^b;   //a=11111111 b = b^a;   //b=11110000 a = a^b;   //a=00001111

    3. 在匯編語言中經常用于將變量置零

    ? xor eax,eax

    4. 快速判斷兩個值是否相等

    ? 例如判斷兩個整數a、b是否相等,可通過下列語句實現:
    ? return ((a ^ b) == 0);

    關于.align

    .align是匯編語言指示符。其含義是邊界對齊調整。”2”表示把隨后的代碼或數據的偏移位置調整到地址值最后 2 比特位為零的位置,即按 4(=2^2)字節方式對齊內存地址。不過現在 GNU as 直接寫出對齊的值而非 2 的冪次。使用該指示符的目的是為了提高 32 位 CPU 訪問內存中代碼或數據的效率。

    七、開啟分頁,跳轉到 main()

    Linus 將內核的頁表直接放在頁目錄之后,使用了4個頁表來尋址16MB的物理內存。如果你有多于16MB的內存,就需要在這里進行擴充修改。關于分頁機制,說來話長,不了解的朋友可以參考我的博文:

    x86分頁機制

    Linus 在物理地址0x0處開始存放1頁頁目錄和4頁頁表。頁目錄是系統所有進程公用的,而其后的4頁頁表則屬于內核專用,它們把線性地址 0x000000~0xFFFFFF 一一映射到物理地址 0x000000~0xFFFFFF。

    .org 0x1000 #從偏移 0x1000 處開始放第1個頁表(偏移0開始處將存放頁目錄) pg0:.org 0x2000 #從偏移 0x2000 處開始放第2個頁表 pg1:.org 0x3000 #從偏移 0x3000 處開始放第3個頁表 pg2:.org 0x4000 #從偏移 0x4000 處開始放第4個頁表 pg3:.org 0x5000 #定義下面的內存數據塊從偏移 0x5000 處開始

    .ORG偽指令用來表示起始的偏移地址,緊接著ORG的數值就是偏移地址的起始值。ORG偽操作常用來指定數據的存儲地址,有時也用來指定代碼段的起始地址。更詳細的解釋可以參考我的博文:

    ORG 偽指令

    tmp_floppy_area:.fill 1024,1,0 #共保留1024項,每項1字節,填充數值0

    fill偽指令的格式是 .fill repeat,size,value
    表示產生 repeat 個大小為 size 字節的重復拷貝。size 最大是 8,size 字節的值是 value.

    “當 DMA (直接存儲器訪問)不能訪問緩沖塊時,tmp_floppy_area 內存塊就可供軟盤驅動程序使用。其地址需要對齊,這樣就不會跨越 64KB 邊界。”
    這是書上的話,我不甚理解。暫不深究,以后再說。

    為調用main()函數做準備

    after_page_tables:pushl $0 # These are the parameters to main :-)pushl $0pushl $0pushl $L6 # return address for main, if it decides to.pushl $mainjmp setup_paging # 設置頁目錄和頁表,并開啟分頁 L6:jmp L6 # main should never return here, but# just in case, we know what happens.

    2~6行:為跳轉到 init/main.c 中的 main() 函數作準備工作。

    2~4行:前3個入棧 0 值應該分別表示 envp、argv 指針和 argc 的值,但 main() 沒有用到。

    第5行:壓入返回地址。模擬調用(其實是使用JMP指令) main.c 程序時首先將返回地址入棧的操作,如果 main.c 程序真的退出,就會返回到標號 L6 處繼續執行下去,即死循環。

    第6行:壓入 main() 函數代碼的地址。當后面執行 ret 指令時,就會彈出 main() 的地址,并把控制權轉移到 init/main.c 程序中。

    依然可以參考我的那篇博文: 在匯編程序中調用C函數

    設置頁目錄和頁表

    setup_paging:movl $1024*5,%ecx # 每個頁表占用1024個雙字(4B),共5個頁表xorl %eax,%eax # eax = 0xorl %edi,%edi # edi = 0cldrep;stosl # eax -> es:[edi],edi每次增加4,重復ecx次movl $pg0+7,pg_dir /* set present bit/user r/w */movl $pg1+7,pg_dir+4 movl $pg2+7,pg_dir+8 movl $pg3+7,pg_dir+12 movl $pg3+4092,%edimovl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */std 1: stosl /* fill pages backwards - more efficient :-) */subl $0x1000,%eaxjge 1bxorl %eax,%eax /* pg_dir is at 0x0000 */movl %eax,%cr3 /* cr3 - page directory start */movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 /* set paging (PG) bit */ret /* this also flushes prefetch-queue */

    2~5行,把頁目錄和頁表清零。

    stosl:Store EAX at address ES:EDI

    7~10行,填寫頁目錄表的前4項。關于表項的格式,可以參考我的博文 頁目錄項和頁表項

    位名稱含義
    0P存在位。為1表示頁表或者頁位于內存中,為0表示不在內存中,必須先予以創建或者從磁盤調入內存后方可使用。
    1R/W讀寫標志。為1表示頁面可以被讀寫,為0表示只讀。當處理器運行在0、1、2特權級時,此位不起作用。頁目錄中的這個位對其所映射的所有頁面起作用。
    2U/S用戶/超級用戶標志。為1時,允許所有特權級別的程序訪問;為0時,僅允許特權級為0、1、2的程序訪問。頁目錄中的這個位對其所映射的所有頁面起作用。

    所以,根據上表,可以知道7~10行中的“+7”表示:頁表存在,可讀可寫,允許所有特權級別的程序訪問。

    填寫后示意圖如下:

    movl $pg3+4092,%edimovl $0xfff007,%eax std # 設置方向位DF=1 1: stosl # Store EAX at address ES:EDIsubl $0x1000,%eax # 更新下一個表項的值,因為一個表項對應 0x1000B 的內存,所以要把頁基址減去0x1000jge 1b # 用于有符號數大小的比較,eax 大于等于 0x1000 則跳轉到1處

    以上代碼的目的是填寫4個頁表。

    頁表項的格式如下圖,0、1、2比特位的含義見前文的表格。

    movl $pg3+4092,%edi

    一張頁表最多可以容納1024個表項,每項占4個字節。下圖左邊是表項的序號,從0到1023,右邊是偏移地址(= 序號*4),4092是最后一個表項的偏移地址。

    上面的代碼表示把頁表3(最后一個頁表)的最后一項的地址傳入edi. 作者的意圖是從最后一個表項開始,倒著填寫,直到填完頁表0的第0個表項。

    movl $0xfff007,%eax中的0xfff007是頁表3的最后一項的值,“7”就不用再解釋了,解釋一下為什么是0xfff000:

    頁表的每一項對應4KB(2^12=4K)的內存,一個頁表有1024(=1K)項,共對應4KB*1K=4MB的內存。代碼中安排了4個頁表,即共可以映射4*4MB=16M內存。

    16M - 4K = 0xFFF000

    或者:

    16M - 1 = 0x1000000-1 = 0xFFFFFF

    0xFFFFFF & 0xFFFFF000 = 0xFFF000

    jge 用于有符號數大小的比較,當 DEST(這里是eax) 大于等于 SRC(這里是0x1000) 則跳轉。當 eax=0x1007時,eax>=0x1000,跳轉之后 eax=0x0007,這時候條件不再成立,則結束跳轉。所以,最后填寫的表項值是 0x0007。

    xorl %eax,%eax /* pg_dir is at 0x0000 */movl %eax,%cr3 # 把頁目錄的物理地址寫入CR3movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 #以上三行使 CR0 的 PG=1, 開啟分頁機制ret

    CR3寄存器的格式如下:

    從movl %eax,%cr0執行后,段部件產生的地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址。

    注意,現在內核工作在分頁機制的一個特殊情況下,線性地址和經過頁部件轉換后的物 理地址相同,這是作者精心安排的。

    最后的ret指令有2個作用。

  • 在改變分頁處理標志后要求使用轉移指令刷新預取指令隊列,這里用的是返回指令ret。

  • 將之前壓入棧中的 main() 程序入口地址彈出,并跳轉到 init/main.c 程序去運行。

    ?

  • 本程序到這里就分析結束了。

    參考資料

    《Linux內核完全剖析》(趙炯,機械工業出版社,2006)

    與50位技術專家面對面20年技術見證,附贈技術全景圖

    總結

    以上是生活随笔為你收集整理的head.s 分析——Linux-0.11 学习笔记(三)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 国产黄片一区二区三区 | 四虎综合 | av手机在线观看 | 欧美高清一区 | 亚洲av成人无码久久精品老人 | 亚洲欧美激情一区二区三区 | 亚洲一卡二卡在线 | 亚洲一区二区在线免费 | 日韩视频一区二区三区 | 久久精品在线播放 | 操天天操| 国产传媒欧美日韩 | 青青视频一区二区 | 久青草影院 | 久久久久亚洲av片无码 | 狠狠老司机 | 国产成人精品一区二三区四区五区 | 色屁屁草草影院ccyycom | 91人妻一区二区三区 | 日本精品影院 | 成人片免费看 | 女人av| 荒岛淫众女h文小说 | 经典杯子蛋糕日剧在线观看免费 | 亚洲女人天堂网 | 福利视频三区 | 亚洲三级视频 | 日韩一级淫片 | 台湾佬中文在线 | 久久伊人超碰 | 国产一级淫片a视频免费观看 | 91原创视频 | 亚洲AV无码精品自拍 | 国产在线激情 | 日韩福利在线播放 | 亚洲av鲁丝一区二区三区 | 国产精品jizz视频 | 一级片一级| 色视频在线观看免费 | 激情综合丁香五月 | a猛片免费播放 | 91爱爱com | 私人网站| 精品3p| 日韩精品视频免费 | 国产一区精品久久 | 波多野吉衣一区二区 | 国产娇小hdxxxx乱 | 超碰在线网址 | 中文字幕一区二区人妻在线不卡 | 精品久久影视 | 亚洲一区二区三区四区五区午夜 | 白白色在线播放 | 久久久久无码国产精品一区 | 国产 福利 在线 | 久草福利网 | 天天操天天草 | 久久免费观看视频 | 精品国产自在精品国产精小说 | 操色网| 又黄又爽又色视频 | 桃色成人网 | 国产伦理一区二区三区 | 中文字幕视频在线 | 免费看女生隐私 | 色狠狠综合网 | 欧美草草 | a天堂av | 成人国产精品一区二区 | 大乳巨大乳j奶hd | 中文字幕久久一区 | 成人禁污污啪啪入口 | 香蕉影院在线 | 蜜桃视频一区二区三区 | 玖玖色资源 | 成人无码精品1区2区3区免费看 | 成人免费视频大全 | 天天天天天操 | 色哟哟欧美精品 | 青青草免费观看视频 | 日本少妇一级片 | 久久青青视频 | 亚洲精品久久久久久无码色欲四季 | 在线中文字幕一区 | 女尊高h男高潮呻吟 | 91伊人 | 91色| 美女久久久久 | 国产成人免费在线观看 | 中国美女一级黄色片 | 久久午夜夜伦鲁鲁片无码免费 | 国产91沙发系列 | 怡红院一区 | 天天干,天天操 | 亚洲激情在线观看 | 777精品伊人久久久久大香线蕉 | 天天天天 | 一级片一区 | 亚洲av激情无码专区在线播放 |