天天看點

哈工大2018計算機系統大作業——程式人生

摘 要

本文主要講述了hello.c程式在編寫完成後運作在linux中的生命曆程,借助相關工具分析預處理、編譯、彙編、連結等各個過程在linux下實作的原理,分析了這些過程中産生的檔案的相應資訊和作用。并介紹了shell的記憶體管理、IO管理、程序管理等相關知識,了解了虛拟記憶體、異常信号處理等相關内容。

關鍵詞:預處理;編譯;彙編;連結;shell;

(摘要0分,缺失-1分,根據内容精彩稱都酌情加分0-1分)

目 錄

第1章 概述 - 4 -

1.1 Hello簡介 - 4 -

1.2 環境與工具 - 4 -

1.3 中間結果 - 4 -

1.4 本章小結 - 4 -

第2章 預處理 - 5 -

2.1 預處理的概念與作用 - 5 -

2.2在Ubuntu下預處理的指令 - 5 -

2.3 Hello的預處理結果解析 - 5 -

2.4 本章小結 - 5 -

第3章 編譯 - 6 -

3.1 編譯的概念與作用 - 6 -

3.2 在Ubuntu下編譯的指令 - 6 -

3.3 Hello的編譯結果解析 - 6 -

3.4 本章小結 - 6 -

第4章 彙編 - 7 -

4.1 彙編的概念與作用 - 7 -

4.2 在Ubuntu下彙編的指令 - 7 -

4.3 可重定位目标elf格式 - 7 -

4.4 Hello.o的結果解析 - 7 -

4.5 本章小結 - 7 -

第5章 連結 - 8 -

5.1 連結的概念與作用 - 8 -

5.2 在Ubuntu下連結的指令 - 8 -

5.3 可執行目标檔案hello的格式 - 8 -

5.4 hello的虛拟位址空間 - 8 -

5.5 連結的重定位過程分析 - 8 -

5.6 hello的執行流程 - 8 -

5.7 Hello的動态連結分析 - 8 -

5.8 本章小結 - 9 -

第6章 hello程序管理 - 10 -

6.1 程序的概念與作用 - 10 -

6.2 簡述殼Shell-bash的作用與處理流程 - 10 -

6.3 Hello的fork程序建立過程 - 10 -

6.4 Hello的execve過程 - 10 -

6.5 Hello的程序執行 - 10 -

6.6 hello的異常與信号處理 - 10 -

6.7本章小結 - 10 -

第7章 hello的存儲管理 - 11 -

7.1 hello的存儲器位址空間 - 11 -

7.2 Intel邏輯位址到線性位址的變換-段式管理 - 11 -

7.3 Hello的線性位址到實體位址的變換-頁式管理 - 11 -

7.4 TLB與四級頁表支援下的VA到PA的變換 - 11 -

7.5 三級Cache支援下的實體記憶體通路 - 11 -

7.6 hello程序fork時的記憶體映射 - 11 -

7.7 hello程序execve時的記憶體映射 - 11 -

7.8 缺頁故障與缺頁中斷處理 - 11 -

7.9動态存儲配置設定管理 - 11 -

7.10本章小結 - 12 -

第8章 hello的IO管理 - 13 -

8.1 Linux的IO裝置管理方法 - 13 -

8.2 簡述Unix IO接口及其函數 - 13 -

8.3 printf的實作分析 - 13 -

8.4 getchar的實作分析 - 13 -

8.5本章小結 - 13 -

結論 - 14 -

附件 - 15 -

參考文獻 - 16 -

第1章 概述

1.1 Hello簡介

P2P:From Program to Process

用進階語言編寫得到.c檔案,再經過編譯器預處理得到.i檔案,進而對其進行編譯得到.s彙編語言檔案。此後通過彙編器将.s檔案翻譯成機器語言,将指令打包成可重定位的.o目标檔案,再通過連結器與庫函數連結得到可執行檔案hello,執行此檔案,作業系統會為其fork産生子程序,再調用execve函數加載程序。至此,P2P結束。

020:From Zero-0 to Zero-0

作業系統調用execve後映射虛拟記憶體,先删除目前虛拟位址的資料結構并未hello建立新的區域結構,進入程式入口後載入實體記憶體,再進入main函數執行代碼。執行完成後,父程序回收hello程序,核心删除相關資料結構。

1.2 環境與工具

Intel(R)Core™i5-7300HQ CPU 2.50GHz 2.50GHz 8G RAM

Win10教育版 64位

虛拟機VMware Workstation Pro12.0

Ubuntu18.4

gcc ld readelf gedit objdump edb hexedit

1.3 中間結果

hello.i:預處理生成的文本檔案

hello.s:.i編譯後得到的彙編語言檔案

hello.o:.s彙編後得到的可重定位目标檔案

Hello:.o經過連結生成的可執行目标檔案

1.4 本章小結

本章主要介紹了P2P、020的過程并列出實驗基本資訊

(第1章0.5分)

第2章 預處理

2.1 預處理的概念與作用

概念:預處理器執行以#開頭的指令(宏定義、條件編譯、讀取頭檔案)、删除注釋等來修改c程式生成.i檔案

作用:1、用實際值替換宏定義的字元串2、檔案包含:将頭檔案中的代碼插入到新程式中3、條件編譯:根據if後面的條件決定需要編譯的代碼

2.2在Ubuntu下預處理的指令

gcc -E hello.c -o hello.i

圖2.2.1預處理指令

2.3 Hello的預處理結果解析

預處理得到.i檔案打開後發現得到了擴充,到3118行。原檔案中的宏進行了宏展開,頭檔案的内容得到引入。

2.4 本章小結

了解了預處理的概念和作用及ubuntu下預處理指令,并分析了.i檔案所包含的資訊。

(第2章0.5分)

第3章 編譯

3.1 編譯的概念與作用

概念:編譯是利用編譯程式從預處理文本檔案(.i)産生彙程式設計式(.s)的過程。

作用:進行詞法分析、文法分析、目标代碼的生成,檢查無誤後生成彙編語言。

3.2 在Ubuntu下編譯的指令

gcc -S hello.i -o hello.s

圖3.2.1編譯指令

3.3 Hello的編譯結果解析

3.3.1彙編檔案指令

指令 内容

.file 聲明源檔案

.text 聲明代碼段

.data 聲明資料段

.section.rodata 隻讀資料,rodata節

.globl 全局變量

.size 聲明大小

.type 指定類型

.align 聲明對指令或資料的存放位址進行對齊的方式

3.3.2整型

1)int sleepsecs:這是個已經初始化的全局變量,存放于.data節中。

2)Main函數的參數argc:函數第一個參數,占四個位元組,儲存于棧空間中

3.3.3字元串

輸出字元串作為全局變量儲存,存儲與.rodata節中。.s檔案中兩個字元串均為printf的參數。

3.3.4數組

有兩個參數int argc,char *argv[]。argv作為存放char指針的數組為第二個參數。

圖3.3.1參數的位址

3.3.5指派

将指派操作編譯為彙編指令MOV,根據資料類型由movb、movw、movl、movq、movabsq。如圖3.3.1也為指派操作。

3.3.6算術和邏輯運算

圖3.3.2算術和邏輯操作

.s中開辟棧空間、獲得參數argv[1]和argv[2]及循環中i++均用到上述操作中的指令。

3.3.7控制轉移

圖3.3.3跳轉指令

.c中比較argc與3時用到跳轉,在.s中則為:

圖3.3.4比較

不相等時:

圖3.3.5不等時跳轉

3.4 本章小結

本章介紹了編譯器如何處理c程式,将預處理文本檔案.i翻譯成.s。介紹了彙編語言的相關知識。

(第3章2分)

第4章 彙編

4.1 彙編的概念與作用

概念:将.s彙程式設計式翻譯成機器語言指令并将這些指令打包成可重定位目标程式的格式存放在hello.o中。

作用:将彙編代碼轉為機器指令,使其在連結後能被機器識别并執行。

4.2 在Ubuntu下彙編的指令

gcc –c –o hello.o hello.s

圖4.2.1生成hello.o

4.3 可重定位目标elf格式

使用readelf -a hello.o > hello.elf 指令獲得hello.o檔案的ELF格式。

圖4.3.1Elf頭

1)Elf 頭:以一個16位元組序列開始,這個序列描述了生成該檔案的系統的字的大小和位元組順序,剩下的資訊幫助連結器文法分析和解釋目标檔案的資訊。包括ELF頭大小,目标檔案類型,機器類型,節頭部表的檔案偏移以及節頭部表中條目的大小和數量。

圖4.3.2節頭部表

2)節頭部表:記錄了各節名稱、類型、位址、偏移量、大小、全體大小、旗标、連接配接、資訊、對齊資訊。

圖4.3.3

3)重定位節:.rela.text ,一個.text節中位置的清單,包含.text節中需要進行重定位的資訊,當連結器把這個目标檔案和其他檔案組合時,需要修改這些位置。調用本地函數的指令則不需要修改。連結器會依據重定向節的資訊對可重定向的目标檔案進行連結得到可執行檔案。

圖4.3.3符号表

4)符号表:.symtab存放着程式中定義和引用函數和全局變量的資訊。且不包含局部變量的條目。重定位中的符号類型全在該表中有聲明。

4.4 Hello.o的結果解析

圖4.4.1反彙編指令

使用指令objdump -d -r hello.o > hellores.txt獲得反彙編代碼,與hello.s比較發現以下差别:

1)分支轉移:在彙編代碼中,分支跳轉是直接以.L0等助記符表示,但是在反彙編代碼中,分支轉移表示為主函數+段内偏移量。反彙編代碼跳轉指令的操作數使用的不是段名稱如.L3,因為段名稱隻是在彙編語言中便于編寫的助記符,是以在彙編成機器語言之後顯然不存在,而是确定的位址。

圖4.4.2分支轉移的比較

2)函數調用:彙編代碼中函數調用時直接跟函數名稱,而在反彙編的檔案中call之後加main+偏移量(定位到call的下一條指令)。在.rela.text節中為其添加重定位條目等待連結。

3)通路全局變量:彙編代碼中使用.LC0(%rip),反彙編代碼中為0x0(%rip)。因為通路時需要重定位,是以初始化為0并添加重定位條目。

4.5 本章小結

本章介紹了從.s到.o的過程。通過彙編檔案和elf格式與.s比較了解機器語言和彙編語言的映射關系。

(第4章1分)

第5章 連結

5.1 連結的概念與作用

概念:連結是将各種代碼個資料片段收集并組合成一個單一檔案的過程,這個檔案可被加載到記憶體并執行。

作用:連結可以執行于編譯時,也就是在源代碼被編譯成機器代碼時;也可以執行于加載時,也就是在程式被加載器加載到記憶體并執行時;甚至于運作時,也就是由應用程式來執行。連結是由叫做連結器的程式執行的。連結器使得分離編譯成為可能。

5.2 在Ubuntu下連結的指令

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

圖5.2.1使用連結指令生成可執行程式hello

5.3 可執行目标檔案hello的格式

使用readelf -a hello >hello1.elf生成hello的ELF格式檔案

圖5.3.1elf頭

圖5.3.2節頭表

5.4 hello的虛拟位址空間

圖5.4.1elf檔案中的程式頭

PHDR 儲存程式頭表

INTERP 動态連結器的路徑

LOAD 可加載的程式段

DYNAMIN 儲存了由動态連接配接器使用的資訊

NOTE 儲存輔助資訊

GNU STACK 标志棧是否可執行

GNU RELRO 指定重定位後需被設定成隻讀的記憶體區域

通過edb加載hello從Data Dump中檢視虛拟位址空間

圖5.4.2虛拟位址空間

在0x400000~0x401000段中,程式被載入,自虛拟位址0x400000開始,到0x400fff結束,這之間每個節的位址同圖5.3.2中的位址聲明。

5.5 連結的重定位過程分析

使用指令objdump -d -r hello > helloout.txt 生成反彙編檔案。

與hello.o生成的反彙編檔案對比發現,helloout.txt中多了許多節。Hellores.txt中隻有一個.text節,而且隻有一個main函數,函數位址也是預設的0x000000。Helloouttxt中有.init,.plt,.text三個節,而且每個節中有許多函數.庫函數的代碼都已經連結到了程式中,程式各個節變得更加完整,跳轉的位址也具有參考性。

圖5.5.1hello生成的反彙編檔案

圖5.5.2hello.o生成的反彙編檔案

5.6 hello的執行流程

使用edb執行hello,單步調試運作。

程式名稱 位址

ld-2.27.so!_dl_start 0x7fce 8cc38ea0

ld-2.27.so!_dl_init 0x7fce 8cc47630

hello!_start 0x400500

libc-2.27.so!__libc_start_main 0x7fce 8c867ab0

hello!main 0x400532

[email protected] 0x4004b0

[email protected] 0x4004e0

5.7 Hello的動态連結分析

在調用共享庫函數時,編譯器沒有辦法預測這個函數的運作時位址,因為定義它的共享子產品在運作時可以加載到任意位置。正常的方法是為該引用生成一條重定位記錄,然後動态連結器在程式加載的時候再解析它。GNU編譯系統使用延遲綁定,将過程位址的綁定推遲到第一次調用該過程時。

5.8 本章小結

本章主要介紹從可重定位檔案hello.o生成可執行檔案hello的過程,讨論了連結過程中對程式的處理。

(第5章1分)

第6章 hello程序管理

6.1 程序的概念與作用

概念:程序是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關,是作業系統結構的基礎。

作用:程序作為一個執行中程式的執行個體,系統中每個程式都運作在某個程序的上下文中,上下文是由程式正确運作所需的狀态組成的。這個狀态包括存放在記憶體中的程式的代碼和資料,它的棧、通用目的寄存器的内容、程式計數器、環境變量以及打開檔案描述符的集合。

6.2 簡述殼Shell-bash的作用與處理流程

作用:是一種互動型的應用級程式,時Linux的外殼,提供了一個界面,使用者可以通過這界面通路作業系統核心。

流程:

1)從終端讀入輸入的指令。

2)将輸入字元串切分獲得所有的參數

3)如果是内置指令則立即執行

4)否則調用相應的程式為其配置設定子程序并運作

5)shell應該接受鍵盤輸入信号,并對這些信号進行相應處理

6.3 Hello的fork程序建立過程

在終端中輸入指令後,shell會處理該指令,判斷出不是内置指令,則會調用fork函數建立一個新的子程序,子程序幾乎但不完全與父程序相同。通過fork函數,子程序得到與父程序使用者級虛拟位址空間相同的但是獨立的一份副本。

6.4 Hello的execve過程

fork之後子程序調用execve函數在目前程序的上下文中加載并運作一個新程式即hello程式。execve加載并運作可執行目标檔案,且帶參數清單argv和環境變量清單envp,并将控制傳遞給main函數。

6.5 Hello的程序執行

邏輯控制流:一系列程式計數器PC的值的序列叫做邏輯控制流,這些值唯一地對應于包含在程式的可執行目标檔案中的指令,或是包含在運作時動态連結到程式的共享對象中的指令。

 時間片:一個程序執行它的控制流的一部分的每一時間段叫做時間片。

 使用者模式和核心模式:shell使得使用者可以有機會修改核心,是以需要設定一些防護措施來保護核心,如限制指令的類型和可以作用的範圍。

上下文切換:上下文就是核心重新啟動一個被搶占的程序所需要的狀态,是一種比較高層次的異常控制流。

開始Hello運作在使用者模式,收到信号後進入核心模式,運作信号處理程式,之後再傳回使用者模式。運作過程中,cpu不斷切換上下文,使運作過程被切分成時間片,與其他程序交替占用cpu,實作程序的排程。

6.6 hello的異常與信号處理

運作過程中可能出現的異常種類由四種:中斷、陷阱、故障、終止。

中斷:來自I/O裝置的信号,異步發生。硬體中斷的異常處理程式被稱為中斷處理程式。

陷阱:是執行一條指令的結果。調用後傳回到下一條指令。

故障:由錯誤情況引起,可能能被修正。修正成功則傳回到引起故障的指令,否則終止程式。

終止:不可恢複,通常是硬體錯誤,這個程式會被終止。

圖6.6.1正常運作

圖6.6.2ctrl+c終止

圖6.6.3ctrl+z暫停

圖6.6.4運作途中

6.7本章小結

本章介紹了程序的相關概念,描述了hello子程序fork和execve的過程,介紹了shell的一般處理流程和異常與信号處理。

(第6章1分)

第7章 hello的存儲管理

7.1 hello的存儲器位址空間

邏輯位址:格式為“段位址:偏移位址”,是CPU生成的位址,在内部和程式設計使用,并不唯一。

實體位址:加載到記憶體位址寄存器中的位址,記憶體單元的真正位址。CPU通過位址總線的尋址,找到真實的實體記憶體對應位址。在前端總線上傳輸的記憶體位址都是實體記憶體位址。

虛拟位址:保護模式下程式通路存儲器所用的邏輯位址。

線性位址:邏輯位址向實體位址轉化過程中的一步,邏輯位址經過段機制後轉化為線性位址。

7.2 Intel邏輯位址到線性位址的變換-段式管理

分段功能在實模式和保護模式下有所不同。

實模式:邏輯位址=線性位址=實際的實體位址。段寄存器存放真實段基址,同時給出32位位址偏移量,則可以通路真實實體記憶體。

保護模式:線性位址還需要經過分頁機制才能夠得到實體位址,線性位址也需要邏輯位址通過段機制來得到。

段寄存器用于存放段選擇符,通過段選擇符可以得到對應段的首位址。處理器在通過段式管理尋址時,首先通過段描述符得到段基址,然後與偏移量結合得到線性位址,進而得到了虛拟位址。

7.3 Hello的線性位址到實體位址的變換-頁式管理

将各程序的虛拟空間劃分成若幹個長度相等的頁(page),頁式管理把記憶體空間按頁的大小劃分成片或者頁面(page frame),然後把頁式虛拟位址與記憶體位址建立一一對應頁表,并用相應的硬體位址變換機構,來解決離散位址變換問題。頁式管理采用請求調頁或預調頁技術實作了内外存存儲器的統一管理。

7.4 TLB與四級頁表支援下的VA到PA的變換

為減少時間開銷,MMU中存在一個關于PTE的緩存,成為翻譯後備緩沖器TLB。其中每一行都儲存着一個由單個PTE組成的塊。TLB通常有高度的相聯度。

圖7.4.1TLB命中與不命中操作圖

7.5 三級Cache支援下的實體記憶體通路

1、CPU給出VA

2、MMU用VPN到TLB中找尋PTE,若命中,得到PA;若不命中,利用VPN(多級頁表機制)到記憶體中找到對應的實體頁面,得到PA。

3、PA分成PPN和PPO兩部分。利用其中的PPO,将其分成CI和CO,CI作為cache組索引,CO作為塊偏移,PPN作為tag。

先通路一級緩存,不命中時通路二級緩存,再不命中通路三級緩存,再不命中通路主存,如果主存缺頁則通路硬碟

7.6 hello程序fork時的記憶體映射

Fork函數被調用是核心為hello新程序建立虛拟記憶體和各種資料結構并為其配置設定唯一的PID。為進行以上操作,還建立了目前程序的mm_struct、區域結構和樣表的原樣副本。Shell将兩個程序中每個頁面都标為隻讀,并将每個程序中的每個區域結構标記為寫時複制。

7.7 hello程序execve時的記憶體映射

删除已存在的使用者區域。

映射私有區域:為新程式的代碼、資料、.bss和棧區域建立新的區域結構。

映射共享區:hello與系統執行檔案連結映射到共享區域。

設定程式計數器PC:設定目前程序上下文中的PC,指向代碼區域的入口點。

7.8 缺頁故障與缺頁中斷處理

缺頁故障是一種常見的故障,要通路的首頁不在主存,需要作業系統調入才能通路。缺頁中斷處理函數為do_page_fault函數。

圖7.8.1缺頁故障處理流程

7.9動态存儲配置設定管理

圖7.9.1動态記憶體配置設定

配置設定器将堆視為一組不同大小的 塊(blocks)的集合來維護,每個塊要麼是已配置設定的,要麼是空閑的。配置設定器的類型有兩種:

顯式配置設定器:要求應用顯式地釋放任何已配置設定的塊.例如,C語言中的 malloc 和 free

隐式配置設定器:應用檢測到已配置設定塊不再被程式所使用,就釋放這個塊

圖7.9.2記錄空閑塊的方法

圖7.9.3配置設定政策

7.10本章小結

本章了解了存儲器位址空間,段式管理和頁式管理,介紹了動态記憶體配置設定和程序的建立及fork和execve時的記憶體映射,還介紹了缺頁故障和缺頁中斷處理。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO裝置管理方法

裝置的模型化:所有的IO裝置都被模型化為檔案,而所有的輸入和輸出都被當做對相應檔案的讀和寫來執行,這種将裝置優雅地映射為檔案的方式,允許 Linux 核心引出一個簡單低級的應用接口,稱為 Unix I/O。

裝置管理:unix io接口

8.2 簡述Unix IO接口及其函數

Linux以檔案的方式對I/O裝置進行讀寫,将裝置均映射為檔案。對檔案的操作,核心提供了一種簡單、低級的應用接口,即Unix I/O接口。

打開檔案:int open(char *filename, int flags, mode_t mode);

關閉檔案:int close(int fd);

讀檔案:ssize_t read(int fd, void *buf, size_t n);

寫檔案:ssize_t write(int fd, const void *buf, size_t n);

8.3 printf的實作分析

從vsprintf生成顯示資訊,到write系統函數,到陷阱-系統調用 int 0x80或syscall.

Syscall将字元串中的位元組以ASCII字元形式從寄存器複制到顯示卡的顯存中。

字元顯示驅動子程式:從ASCII到字模庫到顯示vram(存儲每一個點的RGB顔色資訊)。

顯示晶片按照重新整理頻率逐行讀取vram,并通過信号線向液晶顯示器傳輸每一個點(RGB分量)。

8.4 getchar的實作分析

異步異常-鍵盤中斷的處理:鍵盤中斷處理子程式。接受按鍵掃描碼轉成ascii碼,儲存到系統的鍵盤緩沖區。

getchar等調用read系統函數,通過系統調用讀取按鍵ascii碼,直到接受到Enter鍵才傳回。

8.5本章小結

本章主要介紹了Linux的IO裝置管理方法、Unix IO接口及函數,分析了printf和getchar函數。

(第8章1分)

結論

hello程式的過程可總結如下:

1、編寫代碼:用進階語言寫.c檔案

2、預處理:從.c生成.i檔案,将.c中調用的外部庫展開合并到.i中

3、編譯:由.i生成.s彙編檔案

4、彙編:将.s檔案翻譯為機器語言指令,并打包成可重定位目标程式hello.o

5、連結:将.o可重定位目标檔案和動态連結庫連結成可執行目标程式hello

6、運作:在shell中輸入指令

7、建立子程序:shell嗲用fork為程式建立子程序

8、加載:shell調用execve函數,将hello程式加載到該子程序,映射虛拟記憶體

9、執行指令:CPU為程序配置設定時間片,加載器将計數器預置在程式入口點,則hello可以順序執行自己的邏輯控制流

10、通路記憶體:MMU将虛拟記憶體位址映射成實體記憶體位址,CPU通過其來通路

11、動态記憶體配置設定:根據需要申請動态記憶體

12、信号:shell的信号處理函數可以接受程式的異常和使用者的請求

13、終止:執行完成後父程序回收子程序,核心删除為該程序建立的資料結構

至此,hello運作結束

(結論0分,缺少 -1分,根據内容酌情加分)

附件

hello.i 預處理得到的文本檔案

hello.s 編譯得到的彙程式設計式

Hello.o 彙編後生成的可重定位目标檔案

hello 連接配接後生成的可執行目标檔案

Hello.elf Hello.o的elf檔案

Hello1.elf hello的elf檔案

Helloout.txt Hello的反彙編檔案

Hellores.txt Hello.o的反彙編檔案

(附件0分,缺失 -1分)

參考文獻

為完成本次大作業你翻閱的書籍與網站等

[1] 蘭德爾 E.布萊恩特,大衛 R.奧哈拉倫. 深入了解計算機系統. 機械工業出版社

[2]https://www.cnblogs.com/pianist/p/3315801.html printf函數實作的深入剖析

[3]https://blog.csdn.net/rabbit_in_android/article/details/49976101 關于邏輯位址、虛拟位址、線性位址、實體位址

[4]https://blog.csdn.net/drshenlei/article/details/4261909 位址轉換與分段

(參考文獻0分,确實 -1分)

繼續閱讀