哈工大CSAPP大作業
第1章 概述
1.1 Hello簡介
hello的原始碼hello.c檔案,要生成可執行檔案,首先要進行預處理,其次要進行編譯生成彙編程式碼,接著進行彙編處理生成目標檔案,目標檔案通過連結器形成一個可執行檔案,可執行檔案需要一個執行環境,它可以在linux下通過shell進行執行,與計算機其他經常檔案同步執行,並通過異常處理機制相應訊號。在執行的過程中,程式通過Intel記憶體管理機制一步步訪問邏輯地址、虛擬地址、實體地址,從而進行資料交換,還可以通過IO機制進行輸入輸出互動
1.2 環境與工具
環境:ubuntu64位 vmware虛擬機器環境中執行
gdb
edb
ida pro
visual studio
hexedit
notepad++
objdump
1.3 中間結果
hello.c源程式
hello.i預處理後的源程式
hello.s彙編程式碼
hello.o目標程式
hello可執行檔案
elfo.txt連結前的elf檔案資訊
elfe.txt連結後的elf檔案資訊
asmo.txt hello.o反編譯結果
asme.txt hello反編譯結果
1.4 本章小結
題太多了,太累了QAQ
(第1章0.5分)
第2章 預處理
2.1 預處理的概念與作用
概念:在編譯之前進行的處理。
作用:1.巨集定義2.檔案包含3.條件編譯
2.2在Ubuntu下預處理的命令
gcc hello.c -E
2.3 Hello的預處理結果解析
hello.c程式中只包含三條預處理指令
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
作用是檔案包含,即包含stdio.h,unistd.h,stdlib.h三個檔案
通過-E指令,只啟用預處理指令,可以看到執行完這三條檔案包含指令的結果:將三個檔案的內容引入,程式碼量提升至約800行(詳見hello.i)
2.4 本章小結
預處理命令,可以在編譯器編譯之前,提前進行一些操作,比如定義常量,還可以進行條件編譯以方便除錯,可以進行檔案引入來匯入一些預先寫好的模組,便於程式的組織和除錯和一些特殊的程式設計技巧的實現,是一項非常有用的功能。
(第2章0.5分)
第3章 編譯
3.1 編譯的概念與作用
編譯(compilation , compile)
利用編譯程式從源語言編寫的源程式產生目標程式的過程。
作用:
把用高階程式設計語言書寫的源程式,翻譯成等價的計算機組合語言
3.2 在Ubuntu下編譯的命令
gcc hello.i -S
3.3 Hello的編譯結果解析
1.int sleepsecs = 2.5
該句定義一個int型別全域性變數sleepsecs,其值為2(向整數向下取整後的結果),在偽彙編檔案中,先定義一個全域性符號sleepsecs,用於標識和連線。
.globl sleepsecs
在.data指令後,描述一些該變數的詳細資訊
.type sleepsecs, @object 將sleepsecs定義為物件型別
.size sleepsecs, 4 佔用4個位元組
sleepsecs: 定義sleepsecs對應的標籤
.long 2 定義為長整型數2
2.main函式
.text 在text節中定義
.globl main 宣告全域性符號global
.type main, @function 將main宣告為函式
隨後定義main:標籤,後跟main函式偽彙編指令
3.int i
這是一個未初始化的區域性變數,它存放在棧中,位置是-4(%rbp)
4.if(argc!=3)
cmpl $3, -20(%rbp)
je .L2
argc作為main的引數,存放在-20(%rbp)的位置,將它與3比較,如果相等就不執行後面大括號的部分
5.printf("Usage: Hello 學號 姓名!\n");
movl $.LC0, %edi
call puts
將.LC0的部分傳入引數,執行printf
.LC0定義在.rodata節中,是一個字元常量
.LC0:
.string "Usage:Hello\345\255\246\345\217\267\345\247\223\345\220\215\357\274\201"
6.exit(1);
movl $1, %edi
call exit
引數為1,執行exit函式
7.for(i=0;i<10;i++)
.L2:
movl $0, -4(%rbp)
jmp .L3
該節初始化i為0跳轉到判斷節
.L3:
cmpl $9, -4(%rbp)
jle .L4
如果滿足條件就跳轉、繼續執行
addl $1, -4(%rbp)
迴圈變數遞增
8. printf("Hello %s %s\n",argv[1],argv[2]);
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
將argv[2]傳給%rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
將argv[1]傳給%rsi
movl $.LC1, %edi
將字串常量"Hello %s %s\n"傳給%edi
movl $0, %eax
將%eax賦0
call printf
執行printf
(以下格式自行編排,編輯時刪除)
9.sleep(sleepsecs);
movl sleepsecs(%rip), %eax
movl %eax, %edi
call sleep
將sleep傳給%edi作為引數,執行sleep函式
10. getchar()
call getchar
執行getchar函式
11. return 0;
leave
釋放堆疊空間
ret
返回
3.4 本章小結
本節涉及到的指令全部為gun彙編程式(gas)的偽彙編指令,相比最後的彙編指令內容更為精簡,方便閱讀、分析。程式將常量放入.rodata節,初始化全域性變數放入.data節,通過標籤定義和跳轉等方式定義許多操作,為後序的彙編和連結生成可執行檔案準備。
(第3章2分)
第4章 彙編
4.1 彙編的概念與作用
概念:
把組合語言翻譯成機器語言的過程稱為彙編
作用:
將組合語言翻譯成機器語言
4.2 在Ubuntu下彙編的命令
gcc -c hello.s
4.3 可重定位目標elf格式
分析hello.o的ELF格式,用readelf等列出其各節的基本資訊,特別是重定位專案分析。
各Section基本資訊:
Name:名稱
Type:型別
Address:地址
Offset:地址偏移量
Size:大小
EntSize:全體大小
Flag:旗標
Link:被重定位的符號所在的符號表的section index
Info:需要被重定位的section的index
Align:對齊資訊
包含重定位資訊的節
offset表示該符號在被重定位的section中的偏移
info的高4個位元組表示該符號在.symtab中的index,低4位元組表示重定位的型別
type表示符號型別
sym.value表示連結過程中將要寫入地址的位置
sym.name表示符號名稱
append追加地址
4.4 Hello.o的結果解析
機器語言是直接用二進位制程式碼指令表達的計算機語言,指令是用0和1組成的一串程式碼,它們有一定的位數,並分成若干段,各段的編碼表示不同的含義。
每一條彙編語句被對映為若干二進位制指令碼,將機器語言的每一條指令符號化:指令碼代之以記憶符號,地址碼代之以符號地址。
在組合語言中,運算元用十進位制表示,而在機器語言中,用十六進位制表示,如hello.o中:
機器語言中命令
4: 48 83 ec 20 sub $0x20,%rsp
在組合語言hello.s中對應為
subq $32, %rsp
objdump -d -r hello.o 分析hello.o的反彙編,並請與第3章的 hello.s進行對照分析。
在組合語言中,分支跳轉、函式呼叫,使用標籤定位位置,而在機器語言中使用地址+偏移量計算要跳轉到的實際地址。
如hello.o中:
6f: 7e c1 jle 32 <main+0x32>
71: e8 00 00 00 00 callq 76 <main+0x76>
在hello.s中對應為:
jle .L4
call getchar
4.5 本章小結
彙編是將計算機不能讀懂的組合語言翻譯成計算機能讀懂的機器語言的不可缺少的重要步驟.
(第4章1分)
_
第5章 連結
5.1 連結的概念與作用
概念:
連結是將各種程式碼和資料片段收集並組合成一個單一檔案的過程,這個檔案可被載入(複製)到記憶體並執行。
作用:
連結在軟體開發中扮演著一個關鍵的角色,因為它使分離編譯成為可能。
5.2 在Ubuntu下連結的命令
ld -o hello -dynamic-linker /lib/ld-linker.so.2 /usr/lib/crt1.o /usr/lib/crti.o -l hello.o /usr/lib/ctrn.o
5.3 可執行目標檔案hello的格式
offset:偏移量
virtaddr:虛擬地址
phyaddr:實體地址
filesiz:檔案中的大小
memsiz:記憶體中的大小
flags:旗標
align:對齊
5.4 hello的虛擬地址空間
實際執行中,所有虛擬地址空間段大小都為0x1000,且一段中包含一個或多個5.3中的程式段。動態連結庫中的檔案對映到記憶體的內容,與hello檔案中對映到記憶體的內容地址間隔較大。程式中還包括[stack],[vvar],[vdso],[vsyscall]等特殊用途的地址段。
5.5 連結的重定位過程分析
不同:
hello中包含一些外部檔案的巨集定義、變數、庫函式和作業系統的啟動程式碼等,且.o檔案.text節從0開始,而可執行檔案.text節並非從0開始。
過程:分為符號解析和重定位兩步
- 符號解析:目標檔案定義和引用符號,每個符號對應於一個函式、一個全域性變數或一個靜態變數。符號解析的目的是將每個符號引用正好和一個符號定義關聯起來
- 重定位:編譯器和彙編器生成從地址0開始的程式碼和資料節。連結器通過把每個符號定義與一個記憶體位置關聯起來,從而重定位這些節,然後修改所有對這些符號的引用,使得它們指向這個記憶體位置。連結器使用匯編器產生的重定位條目的詳細指令,不加甄別地執行這樣的重定位。
重定位:
hello.o的檔案中包含一些重定位條目
這些重定位條目告訴連結器32位PC相對地址或32位絕對地址進行重定位,這些重定位條目通過計算地址或直接呼叫儲存的絕對地址,達到重定位的目的。
無論何時彙編器遇到對最終位置未知的目標引用,會生成一個重定位條目,告訴連結器在將目標檔案合併成可執行檔案時如何修改這個引用。程式碼的重定位條目放在.rel.text中,已初始化資料的條目放在.rel.data中
5.6 hello的執行流程
_start
__libc_start_main
__GI___cxa_atexit
__internal_atexit
__GI___cxa_atexit
__internal_atexit
__new_exitfn
__internal_atexit
__GI___cxa_atexit
__libc_start_main
_setjmp
__sigsetjmp
__sigjmp_save
__libc_start_main
__GI_exit
__run_exit_handlers
__GI___call_tls_dtors
__run_exit_handlers
__do_global_dtors_aux
deregister_tm_clones
__do_global_dtors_aux
_fini
__run_exit_handlers
_IO_cleanup
_IO_unbuffer_all
_IO_cleanup
_IO_flush_all_lockp
_IO_cleanup
_IO_unbuffer_all
_IO_cleanup
_IO_unbuffer_all
_IO_new_file_setbuf
_IO_default_setbuf
_IO_new_file_sync
_IO_default_setbuf
__GI__IO_setb
_IO_default_setbuf
__GI__IO_setb
_IO_default_setbuf
_IO_new_file_setbuf
_IO_unbuffer_all
_IO_new_file_setbuf
_IO_default_setbuf
_IO_new_file_sync
_IO_default_setbuf
__GI__IO_setb
_IO_default_setbuf
__GI__IO_setb
_IO_default_setbuf
_IO_new_file_setbuf
_IO_unbuffer_all
_IO_cleanup
__run_exit_handlers
__GI__exit
5.7 Hello的動態連結分析
動態連結專案如下圖(主要為兩個.so檔案相關內容)
分析GOT的變化
dl_init前:
dl_init後
0x6010208~0x601020d位元組發生了變化
5.8 本章小結
(個人認為,在全書中,“連結”這一章的難度比所有其他章節之和還要大,搞清楚動態連結的全過程十分困難,課本上也有意的跳過了一些部分,導致一些細節問題十分費解。)
連結是組建大型程式和團隊程式設計不可缺少的重要部分,掌握連結器的一些原理和動態連結是非常有必要的,也是學習庫打樁等強大機制的基礎。雖然hello.c很簡單,但是也需要和標準庫進行連結。瞭解hello.c連結的來龍去脈,對掌握連結技術很有幫助。
(第5章1分)
第6章 hello程序管理
6.1 程序的概念與作用
概念:
程序(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。在早期面向程序設計的計算機結構中,程序是程式的基本執行實體;在當代面向執行緒設計的計算機結構中,程序是執行緒的容器。程式是指令、資料及其組織形式的描述,程序是程式的實體。
作用:
由於程式是靜態的,我們看到的程式是儲存在儲存介質上的,它無法反映出程式執行過程中的動態特性,而且程式在執行過程中是不斷申請資源,程式作為共享資源的基本單位是不合適的,所以需要引入一個概念,它能描述程式的執行過程而且可以作為共享資源的基本單位,這個概念就是程序。程序解決了系統資源排程等一系列問題。
6.2 簡述殼Shell-bash的作用與處理流程
Shell俗稱殼(用來區別於核心),是指“提供使用者使用介面”的軟體,就是一個命令列直譯器。
bash 是一個為GNU專案編寫的Unix shell,也就是linux用的shell。
所以Shell-bash的“作用”是linux系統的一個命令列直譯器
流程:
6.3 Hello的fork程序建立過程
shell呼叫fork函式,形成自身的一個拷貝(子程序),為執行hello做準備
6.4 Hello的execve過程
在shell的子程序中執行execve函式,將引數傳給Hello程式,並執行Hello
6.5 Hello的程序執行
一開始,Hello執行在使用者模式,當程式收到一個訊號時,進入核心模式,執行訊號處理程式,之後再返回使用者模式。在Hello執行的過程中,cpu不斷切換上下文,使Hello程式執行過程被切分成時間片,與其他程序交替佔用cpu,實現程序的排程。
6.6 hello的異常與訊號處理
1.輸入Ctrl-C時,程式終止
處理過程是向程式傳送SIGINT訊號,程式執行預設行為:停止執行
2.輸入Ctrl-C時,程式掛起
處理過程是向程式傳送SIGSTP訊號,程式執行預設行為:掛起程式,之後會返回shell中
- 亂按+回車
輸入的內容會被留在緩衝區中,當hello執行結束,返回shell中,shell會從緩衝區讀取並嘗試解析這些內容
- ps jobs pstree fg kill命令
ps:顯示當前程序的狀態
jobs:檢視後臺執行的程序
fg:恢復一個後臺程序
pstree:顯示程序樹
kill:結束一個程序
可能會產生IO中斷、時鐘中斷、系統呼叫等等,會產生SIGINT、SIGSTP等訊號。
。
6.7本章小結
linux命令列shell是一個非常強大的工具,用它可以更方便的執行Hello和傳送各種命令請求。通過訊號等方式可以實現異常處理,讓Hello在順序執行者也能處理一些突發狀況和實現一些功能。程序排程實現了各個程序計算資源合理分配,互不干擾,提高了系統穩定性和效率。
(第6章1分)
第7章 hello的儲存管理
7.1 hello的儲存器地址空間
實體地址(physical address)
用於記憶體晶片級的單元定址,與處理器和CPU連線的地址匯流排相對應。
邏輯地址(logical address)
邏輯地址指的是機器語言指令中,用來指定一個運算元或者是一條指令的地址。如Hello中sleepsecs這個運算元的地址。
線性地址(linear address)或也叫虛擬地址(virtual address)
跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬體平臺段式管理轉換前地址的話,那麼線性地址則對應了硬體頁式記憶體的轉換前地址。
7.2 Intel邏輯地址到線性地址的變換-段式管理
段式記憶體管理方式就是直接將邏輯地址轉換成實體地址,也就是CPU不支援分頁機制。其地址的基本組成方式是段號+段內偏移地址。
在x86保護模式下,段的資訊(段基線性地址、長度、許可權等)即段描述符佔8個位元組,段資訊無法直接存放在段暫存器中(段暫存器只有2位元組)。Intel的設計是段描述符集中存放在GDT或LDT中,而段暫存器存放的是段描述符在GDT或LDT內的索引值(index)。
首先給定一個完整的邏輯地址[段選擇符:段內偏移地址],
1.看段選擇描述符中的T1欄位是0還是1,可以知道當前要轉換的是GDT中的段,還是LDT中的段,再根據指定的相應的暫存器,得到其地址和大小,我們就有了一個數組了。
2.拿出段選擇符中的前13位,可以在這個陣列中查詢到對應的段描述符,這樣就有了Base,即基地址就知道了。
3.把基地址Base+Offset,就是要轉換的下一個階段的地址。
7.3 Hello的線性地址到實體地址的變換-頁式管理
分頁的基本原理是把記憶體劃分成大小固定的若干單元,每個單元稱為一頁(page),每頁包含4k位元組的地址空間(為簡化分析,我們不考慮擴充套件分頁的情況)。這樣每一頁的起始地址都是4k位元組對齊的。為了能轉換成實體地址,我們需要給CPU提供當前任務的線性地址轉實體地址的查詢表,即頁表(page table)。
為了節約頁表佔用的記憶體空間,x86將線性地址通過頁目錄表和頁表兩級查詢轉換成實體地址。32位的線性地址被分成3個部分:最高10位 Directory 頁目錄表偏移量,中間10位 Table是頁表偏移量,最低12位Offset是物理頁內的位元組偏移量。頁目錄表的大小為4k(剛好是一個頁的大小),包含1024項,每個項4位元組(32位),專案裡儲存的內容就是頁表的實體地址。如果頁目錄表中的頁表尚未分配,則實體地址填0。頁表的大小也是4k,同樣包含1024項,每個項4位元組,內容為最終物理頁的實體記憶體起始地址。
每個活動的任務,必須要先分配給它一個頁目錄表,並把頁目錄表的實體地址存入cr3暫存器。頁表可以提前分配好,也可以在用到的時候再分配。
7.4 TLB與四級頁表支援下的VA到PA的變換
7.5 三級Cache支援下的實體記憶體訪問
先訪問一級快取,不命中時訪問二級快取,再不命中訪問三級快取,再不命中訪問主存,如果主存缺頁則訪問硬碟
7.6 hello程序fork時的記憶體對映
執行新程序(hello)時,為這個新程序建立虛擬記憶體
- 建立當前程序的的mm_struct, vm_area_struct和頁表的原樣副本
- 兩個程序中的每個頁面都標記為只讀
- 兩個程序中的每個區域結構(vm_area_struct) 都標記為私有的寫時複製
在新程序中返回時,新程序擁有與呼叫fork程序相同的虛擬記憶體, 隨後的寫操作通過寫時複製機制建立新頁面
7.7 hello程序execve時的記憶體對映
- 刪除已存在的使用者區域
- 建立新的區域結構: 程式碼和初始化資料對映到.text和.data區(目標檔案提供), .bss和棧對映到匿名檔案
- 設定PC,指向程式碼區域的入口點
7.8 缺頁故障與缺頁中斷處理
(以下格式自行編排,編輯時刪除)
缺頁故障:需要訪問的頁不在主存,需要作業系統將其調入後才能訪問。
有三種情況:
只有正常缺頁時,系統才會調入需要訪問的頁,並再次執行訪問該頁的命令。
7.9動態儲存分配管理
基本方法:維護一個虛擬記憶體區域“堆”,將堆視為一組不同大小的 塊(blocks)的集合來維護,每個塊要麼是已分配的,要麼是空閒的,需要時選擇一個合適的記憶體塊進行分配。
- 記錄空閒塊,可以選擇隱式空閒連結串列,顯示空閒連結串列,分離的空閒連結串列和按塊大小排序建立平衡樹
- 放置策略,可以選擇首次適配,下一次適配,最佳適配
- 合併策略,可以選擇立即合併,延遲合併
- 需要考慮分割空閒塊的時機,對內部碎片的忍耐閾值.
7.10本章小結
通過快取記憶體、虛擬記憶體、動態記憶體分配,可以實現快速、高校、利用率高的儲存空間管理。可以通過記憶體對映等方式實現檔案共享。儲存管理是一個相當重要、值得研究的機制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO裝置管理方法
裝置的模型化:將裝置抽象成檔案
裝置管理:通過unix io介面管理
8.2 簡述Unix IO介面及其函式
開啟和關閉檔案:open()and close()
讀寫檔案:read() and write()
改變當前的檔案位置 lseek()
8.3 printf的實現分析
1.從vsprintf生成顯示資訊到write系統函式,到陷阱-系統呼叫 int 0x80或syscall.
2.字元顯示驅動子程式:從ASCII到字模庫到顯示vram(儲存每一個點的RGB顏色資訊)。
3.顯示晶片按照重新整理頻率逐行讀取vram,並通過訊號線向液晶顯示器傳輸每一個點(RGB分量)。
8.4 getchar的實現分析
1.非同步異常-鍵盤中斷的處理:鍵盤中斷處理子程式。接受按鍵掃描碼轉成ascii碼,儲存到系統的鍵盤緩衝區。
2.getchar等呼叫read系統函式,通過系統呼叫讀取按鍵ascii碼,直到接受到回車鍵才返回。
8.5本章小結
輸入輸出看似簡單,實際是一個非常精巧的過程,從程式發出請求到系統函式呼叫到裝置相應,需要執行許多步驟,往往也是拖慢程式的主要因素和一些崩潰異常的高發地,需要謹慎選用函式、命令實現目的。
(第8章1分)
結論
hello的原始碼hello.c檔案,要生成可執行檔案,首先要進行預處理,其次要進行編譯生成彙編程式碼,接著進行彙編處理生成目標檔案,目標檔案通過連結器形成一個可執行檔案,可執行檔案需要一個執行環境,它可以在linux下通過shell進行執行,與計算機其他經常檔案同步執行,並通過異常處理機制相應訊號。在執行的過程中,程式通過Intel記憶體管理機制一步步訪問邏輯地址、虛擬地址、實體地址,從而進行資料交換,還可以通過IO機制進行輸入輸出互動。
通過學習ics這門課程,深感計算機這個龐大體系的複雜、精巧,從電路到電路組合,再到硬體整合、軟體調配,每一處都井井有條、隨處顯現著前人的智慧。不少複雜概念的學習,通過這門課感覺僅僅只是入了個門,距離熟練運用甚至涉及差距仍然較大,但不妨礙進行一些思維上的創新。
目前的電子計算機建立在二進位制基礎上,將來基礎物理突破之後,可能會實現三進位制、四進位制的計算機,沒準可以實現質的飛越。或許神經科學有了巨大飛越,讓“電腦”真正構建起一個類似於人腦的神經結構,實現一個強的人工智慧。