Linux發行版製作指南(轉)

ba發表於2007-08-11
Linux發行版製作指南(轉)[@more@]摘要
   此文件著重介紹了Linux發行版製作過程中的各個步驟。
By Coolee

1.專案整體分析
   製作Linux釋出的目的是為了在系統中能夠快速,正確地建立Linux系統環境。製作Linux釋出的主要工作是決定各種軟體的去留,因為有了RPM(RedHat Package Manager)包對其提供優良的管理能力,所以以目前比較成熟的RedHat 7.1(Linux Kernel Version 2.4.2-12)Linux釋出程式作為藍本,以RPM包作為基本的制定單元,以需求為原則對其進行取捨,得到適合實際需要的Linux系統。

   由此,專案自然而然的以分析RedHat Linux的光碟安裝系統為起點,在掌握了其結構和行為的基礎上,在包一級(結構部分)和程式碼一級(行為部分)進行修改,同時建立相應的測試環境,以便對修改進行及時的規範。

2.專案分步驟實施細節

   2.1對Linux光碟安裝系統的分析

   ●結構部分

   在安裝光碟中,主要的目錄結構和檔案大致如下:

   images/ 此目錄下包含了製作啟動盤的映像檔案(檔案字尾img),

   其中boot.img是當安裝介質為CD-ROM時負責引導系統的映像檔案

   bootnet.img是當安裝介質為FTP,NFS等時負責引導系統的映像檔案

   driver.img是由一些特殊裝置驅動程式模組組成的映像檔案,在當前核心不支援這些裝置的情況下,提供了對它們進行訪問的一種方法

   其中,boot.img映像檔案中主要包含以下檔案:


   boot.img
   |----vmlinuz Linux核心
   |----ldlinux.sys 引導Linux的系統檔案
   |----syslinux.cfg Linux核心引導引數配置檔案
   |----initrd.img 記憶體虛擬檔案系統映像檔案
   |----*.msg檔案 引導時的各種提示資訊檔案



   其中,initrd.img為Linux ext2檔案系統,構成如下:


   initrd.img
   |----/bin
   |----/dev
   |----/etc
   |----/module
   |----/sbin ------ loader
安裝程式裝載器
   |----/tmp
   |----/var



   可執行檔案/sbin/loader的任務是判斷安裝介質的有效性,並從中執行安裝程式。

   其實正是boot.img,在系統啟動時被執行,經解析之後在記憶體建立起了Linux核心,並根據配置檔案syslinux.cfg裝載虛擬檔案系統,形成了完整的Linux System,為後續的工作提供了必要的作業系統環境。Boot.img映像的檔案系統型別為msdos,而其中的initrd.img映像的檔案系統型別必為Linux系統自己的ext2,所以對於它們的解析操作是不同的,具體請參考附錄A 。

   RedHat/ 此目錄是RedHat Linux釋出的核心目錄,主要的目錄結構都在這裡,其中

   RPMS/ 包含了RedHat Linux釋出的主要部分,即以RPM包的形式將Linux系統中的二進位制可執行檔案,配置檔案,文件等等組織在一起,形成能完成一定功能的比較獨立的軟體包(檔案字尾rpm)。這個目錄就是把這些軟體包都集合在一起,形成了RedHat Linux釋出。

   base/ 包含了在安裝過程中要用到的描述組織結構和安裝行為的所有檔案,其中comps,hdlist和hdlist2是描述RPM包組織結構的檔案。

   comps 此檔案把各個RPM包按一定的原則組織成若干組,即components,這樣在安裝過程中就不必對每一個包做出取捨,而以組為單位。comps檔案為簡單文字格式,它的結構如下所示:


   4 表示RPM包的版本號,當前為4
   1 base { }
   base是此component名,{…}中是此component中所包含的RPM包
    的名稱列表,1表示在安裝中預設為選中,即預設安裝。

0 –hide IDS sensor{
         snort
    libpcap
}
     表示IDS sensor組中包含有snort和lipcap這兩個RPM包。0表示
     這個組在安裝中預設為不選中即預設不安裝,並且由—hide指出
     不在使用者介面上顯示此組。



   hdlist和hdlist2 這兩個檔案維護從RPM包名到真實包檔名的對映過程,例如從snort這個RPM包名到真實包檔名snort-1.8.1-1.1.2.i386.rpm的對映。這兩個檔案是用特殊的程式生成的,無法用簡單的方法察看其中的內容和結構。具體的生成方法請參考附錄D。

   stage2.img , hdstg1.img , hdstg2.img , netstg1.img 和netstg2.img 是描述安裝行為的映像檔案,其中

   stage2.img 是當安裝介質為CD-ROM時的安裝程式映像檔案

   hdstg1.img 是當安裝介質為HardDisk時的安裝程式映像檔案

   hdstg2.img 是當安裝介質為HardDisk時的安裝程式映像檔案

   netstg1.img 是當安裝介質為FTP,NFS時的安裝程式映像檔案

   netstg2.img 是當安裝介質為FTP,NFS時的安裝程式映像檔案

這裡主要討論stage2.img的內容


   stage2.img
   |----/etc
|----/modules
|----/proc
|----/usr----/bin----anaconda
安裝程式主執行檔案
|
|------/lib-----/anaconda
安裝程式指令碼檔案目錄
     | |----/installclasses
| |----/iw
| |----/texttw
| |----*.py
|
|------/share---/anaconda
安裝程式資原始檔目錄
| |----/help
| |----/pixmaps



   如上所示,stage2.img映像檔案中的主要部分是安裝程式anaconda,它的主執行體是/usr/bin下的anaconda,由其呼叫的大量例程分佈在/usr/lib/anaconda下,而安裝過程中要用到的資原始檔分佈在/usr/share/anaconda下。stage2.img 的解析方法請參考附錄B。

   ●行為部分

   RedHat 7.1的安裝程式被命名為anaconda。如前所述,當boot.img所代表的啟動介質被系統引導之後,在記憶體中就建立了一個完整的Linux系統(包括Linux核心和一個記憶體虛擬檔案系統),之後便執行檔案系統中存在的loader命令,從適當的介質中執行安裝程式(例:安裝介質是CD-ROM,就解析CD-ROM上的stage2.img,並從中執行安裝程式),即執行anaconda,完成Linux系統的安裝任務。

   此次利用RedHat 7.1的安裝程式原始碼的SRPM包形式:anaconda-7.1-5.src.rpm來獲得anaconda的源程式,經解包後在/usr/src/redhat/SOURCES/anaconda-7.1形成了原始碼樹。


   anaconda-7.1
   |-------------------/bootdisk
啟動盤目錄
   |-------------------/docs
文件目錄
   |-------------------/help
安裝過程幫助系統目錄
   |-------------------/installclasses
安裝型別分類目錄
   |-------------------/iw
安裝各步驟響應目錄
   |-------------------/loader
安裝程式裝載器目錄
   |-------------------/pixmap
圖形資源目錄
   |-------------------/utils
工具目錄
   |-------------------*.py
各Python指令碼檔案



   分析如下:

   anaconda安裝程式主要用Python語言寫成,它是一種解釋性的,物件導向的指令碼語言。原始檔字尾為.py,也可生成可執行的位元組碼,字尾為.pyc或.pyo。其中:

   installclasses/ 子目錄中各檔案定義了在安裝過程中使用者可選擇的安裝型別,通常由四個檔案workstation.py , server.py , laptop.py和custom.py來描述workstation(工作站)安裝型別,server(伺服器)安裝型別,laptop(膝上型電腦)安裝型別和custom(自定義)安裝型別。每個指令碼檔案的內部,則是根據自己安裝型別的特點對安裝步驟,分割槽策略以及包的取捨做出了不同的方案。

   iw/ 子目錄中各檔案定義了在圖形介面安裝狀態時各步驟對Next(下一步)和Prev(上一步)的響應函式。

   loader/ 安裝程式裝載器的原始碼目錄,用C語言寫成。

   pixmap/ 圖形資源目錄,包括安裝過程中使用到的所有點陣圖,圖示。

   utils/ 安裝程式實用工具目錄。

   anaconda 是安裝程式的主執行檔案,它建立了Python語言的執行環境,提供了程式的入口點並以模組的方式將各個子系統結合在一起。

   gui.py 定義了安裝程式圖形介面使用的各種視窗類,包括MessageWindow,ProgressWindow,WaitWindow,ExeceptWindow等,和控制這些視窗及圖形介面行為的InstallInterface, InstallControlWindow, InstallControlState類。總之,控制gui。

   todo.py 定義了安裝程式的各種行為函式,它是圖形介面背後真正進行各項操作的函式集合。

   harddrive.py 定義了當安裝介質為硬碟時,系統該如何找到安裝程式的光碟映像,並從中執行程式。

   安裝程式原始碼的編譯由make和make install組成,完成後在/usr/src/RedHat目錄下形成了如下目錄結構:


   instimage
|------/etc
|------/usr
|------/bin
|------/sbin
|------/lib
| |------/anaconda
| | |------installclasses
| | |------iw
| | |------texttw
| | |------*.py
| |
| |------/anaconda-runtime
| |------/boot/loader
|
|------/share------/anaconda
|------/help
|------/pixmaps



   此目錄結構基本與stage2.img的檔案結構相同。

   2.2除錯環境的建立:

   ●對源程式的修改

   在分析完安裝程式的基本構成之後,就要建立相應的除錯環境。建立此環境的目的是為了可以方便地對修改過的安裝程式及裁減後的RPM包進行隨時的確認。顯然,可以選用CD-ROM或本地硬碟作為除錯介質,下表比較了兩者的差別:

CDROM 硬碟
對應的安裝介面 圖形介面 選單介面
對應的映像檔案 stage2.img *.iso中的hdstg1.img , hdstg2.img
優點 圖形介面,直接使用映像檔案stage2.img 隨改隨調,除錯周期短,效率高
缺點 每次改動都要求刻盤,除錯效率低 選單介面,每次除錯都要求提供光碟映像檔案*.iso,效率上打折扣

   在兩者各有優缺點的情況下,考慮折衷的方案,即為了首先保證除錯的效率,採用硬碟作為除錯介質,但對應的映像檔案選取stage2.img,這樣能達到效率最大化,同時除錯介面採取圖形方式。採用此方案時,須修改原始碼,以達到預期的效果。

   從前面對安裝系統的分析,可以看出在initrd.img中的/sbin/loader程式負責判斷安裝介質的有效性,並從中執行安裝程式。所以要首先修改它的原始碼檔案loader.c,從中找出硬碟安裝時預設讀出光碟映像檔案*.iso的函式setupIsoImages,並註釋掉其中在硬碟目錄中尋找映像檔案*.iso的相關操作,具體對應Line 582 至 Line590行中包含sprintf和if(){}迴圈的語句,以避免開啟子目錄,並在其後加入mountLoopback("/tmp/hdimage/RedHat/base/stage2.img","/mnt/runtime", "loop0");一句以便實現直接使用stage2.img的目的,並註釋掉其後從errno=0開始的程式碼,經過整個while迴圈到closedir(dir),但保留umount(“/tmp/hdimage”);註釋掉if(!net) return NULL;一句。以上操作目的是防止程式讀出光碟映像檔案*.iso。在loader.c的主函式main()中的結尾部分,註釋掉if (!FL_TESTING(flags)) { 和 }的條件判斷的兩條語句,讓程式毫無疑問地執行硬碟上的安裝程式。至此,對loader.c修改完畢。

   同時還要對Python指令碼的一些相關檔案進行修改以保證對stage2.img檔案的支援。具體的,在harddrive.py的類class HardDriveInstallMethod中,註釋掉函式 mountMedia(self, cdNum)中的所有內容並加Pass語句的方法使此函式失效,同樣方法處理umountMedia函式,mountDirectory函式和umountDirectory函式,為了保險起見,在其他函式中註釋掉有關上面函式的呼叫。並在類的建構函式(初始化)中的# Go ahead…語句之前加self.tree=”/tmp/hdimage/”語句,並註釋掉後面的所有語句。這樣做仍然是要保證廢棄iso映像轉而對stage2.img實現控制。不僅如此,最好還註釋掉todo.py中的Line1781至Line1783呼叫self.method.systemMounted一段,以確保不出差錯。接著進行make和make install,重新編譯程式,使修改生效,並把新的loader程式從編譯的目標目錄中copy到boot.img中initrd.img中的相應目錄並覆蓋舊的loader檔案。為了啟動時的快速,修改boot.img中的syslinux.cfg檔案,去掉啟動提示,延時和其他Linux啟動選項,修改後的syslinux.cfg檔案,請參考附錄F

   最後,把boot.img做成啟動盤,方法請參考附錄G。

   ●建立硬碟介質中的除錯目錄

   在硬碟的Linux分割槽中建立形如RedHat安裝光碟目錄結構的除錯目錄及相關檔案,如下所示:
   |----/images
   | |------boot.img
   |
   |----/RedHat
|----/base
   |     |------comps , hdlist , hdlist2,stage2.img
   |
|----/RPMS
   |   |----*.rpm




   建立這種目錄結構和相關檔案的原因是在安裝程式中已經以程式碼的形式確定了它們的命名及結構。其中,對boot.img和stage2.img的相關修改如前所述,而涉及到對comps,hdlist,hdlist2的修改,則需在後續的裁剪過程中確定。

   至此,除錯環境建立完畢。現在可以用做好的啟動盤來引導系統,並且可以從指定的硬碟上測試安裝程式和RPM包的正確性。

2.3對安裝步驟的簡化

   在對RPM 包的剪裁進行之前,還要對原有的安裝步驟做出簡化,去掉一些與系統需求大致無關的專案,使安裝者可以集中精力地配置Sensor的主要引數,忽略諸如對鍵盤,滑鼠,和多國語言的配置。具體的如下所示:

   原有的安裝步驟有:

   1.安裝語言選擇

   2.鍵盤配置

   3.滑鼠配置

   4.歡迎資訊

   5.安裝型別選擇(包括安裝或升級,安裝部分又包括workstation,server,laptop,custom四種型別)

   6.選擇分割槽方式(自動分割槽,手動分割槽,專業分割槽)

   7.選擇以上部分或全部分割槽格式化

   8.Lilo作業系統引導器配置

   9.網路卡及網路配置

   10.防火牆配置

   11.語言配置

   12.時區配置

   13.賬戶配置

   14.認證配置

   15.包組及單RPM包選擇

   16.包獨立性檢查

   17.X-Window配置

   18.安裝前確認

   19.安裝過程

   20.製作啟動盤

   21.安裝完畢確認

   在這些安裝步驟中很多都是在確定了RPM包組及除錯完成後不必要存在的,所以去掉第1,2,3,4,7,8,10,11,14,16,17和第20項安裝步驟,所有去掉步驟的相關設定都採取預設的設定值,如第7步 採取分割槽全部格式化的方案,第8步,採取Lilo放置在MBR上,default boot 為Linux的設定等等,並修改第5步,去掉升級型別和安裝型別中的所有4種既定型別,新增IDS sensor型別。修改完成以後的安裝步驟如下所示:

   1.安裝型別選擇(現有的為IDS sensor一種)

   2.選擇分割槽方式(自動分割槽,手動分割槽,專業分割槽)

   3.網路卡及網路配置

   4.時區配置

   5.賬戶配置

   6.包組及單RPM包選擇

   7.安裝過程

   8.安裝完畢確認

   為此,需要修改Python指令碼語言

   ● 在安裝程式執行之初,需要先禁止掉安裝語言選擇,鍵盤配置,滑鼠配置和歡迎資訊,在anaconda中在判斷語言是否有效之前即Line491 if lang:之前加上以下四句:

[aidcode]   instClass.addToSkipList("language");   instClass.addToSkipList("keyboard");   instClass.addToSkipList("mouse");   instClass.addToSkipList("welcome"); [/aidcode]
   並在gui.py中的類class InstallInterface的run函式中註釋掉Line371至Line371,即在commonSteps結構中除保留( InstallPathWindow, "installtype" )外,註釋掉( LanguageWindow, "language" ),( KeyboardWindow, "keyboard" ),( MouseWindow, "mouse" )和( WelcomeWindow, "welcome" )

   ●對安裝型別進行精簡。在installclasses目錄下去掉upgrateonly.py , workstation.py , server.py , laptop.py和custom.py即去掉升級型別和安裝型別中的所有4種既定型別, 新增IDS sensor型別,即在目錄中新增sensor.py檔案,檔案的具體內容請參考附錄E。

   除此之外,還要修改iw/installpath_gui.py指令碼檔案,以便在圖形介面上不顯示Install和Upgrate圖示和選項,只顯示IDS Sensor專案。具體的,註釋掉Line223,Line 227,Line 233和Line 234程式碼,即在類class InstallPathWindow中的getScreen函式的最後部分不執行顯示。

   ● 去掉其他多餘步驟,主要修改iw/installpath_gui.py指令碼檔案。註釋掉類class InstallPathWindow初始化函式__init__裡結構self.installSteps中的( FormatWindow, "format" ) , ( FirewallWindow, "firewall" ) , ( LanguageSupportWindow, "languagesupport" ) , ( AuthWindow, "authentication" ) , ( UnresolvedDependenciesWindow, "dependencies" ) , ( XConfigWindow, "xconfig" ) , ( BootdiskWindow, "bootdisk" ),即對應著去掉的第7,10,11,14,16,17和第20步驟。

   當這三步完成後,進行make和make install,重新編譯程式,使修改生效,並把這些檔案Copy至stage2.img中的相關位置,替換掉舊檔案。這樣就完成了對安裝步驟的簡化。

   2.4對RPM 包的剪裁

   對RPM包的裁剪依這次的需求進行。專案整體分析時已做出說明,製作目的是為了在系統前端Sensor 中能夠快速,正確地建立執行Snort的Linux系統環境。所以只需保留Linux基本系統和執行snort所需的環境即可。

   Linux基本系統的RPM包組成在comps檔案中的base部分中有詳細的描述,所以就以它為藍本,去掉除base以外的所有component,並去掉base中以下不必要的RPM包:

[aidcode]   apmd     ash   autoconfig   dhcpcd  dosfstools  ed   eject   gdm   gettext   gpm gruff ksymoops lokkit mailcap mailx man mktemp mouseconfig ncurses openldap popt procmail pump raidtool readline redhat-logos redhat-release rootfiles sendmail syslinux utemper words [/aidcode]
   在comps檔案中增加IDS Sensor component並在此組中新增必要的RPM包,如snort-1.8.1就需要snort , libpcap , mysql , openssl , openssl-clients , perl等RPM包,除此之外,為了方便除錯,也保留了一些用於操作和診斷的RPM包,如tcpdump , iputils , zip等,完成的comps請參考附錄C。

   同時,在/RPM目錄中依照上面所有確認保留的RPM包名稱,刪除不予保留的各RPM檔案(檔案字尾.rpm)。此時在/base中的comps檔案和在/RPM中的各rpm檔案都已剪裁完成,可以利用genhdlist生成hdlist和hdlist2檔案了,genhdlist 在anaconda-7.1的原始碼中的utils目錄裡。具體生成方法見附錄D。

3. 存在的問題及今後目標

   此次對NetCop Linux釋出的製作是以RPM包作為裁剪單元進行的,所以必然存在很大缺陷,即對RPM包的內部毫無辦法,無法去掉RPM包內部對需求無用的大量檔案並且無法對核心,指令碼檔案等做出制定修改。所以,下一步所要做的工作,是逐一開啟每一個候選的RPM包,依需求對包內的每一個檔案做出修改或丟棄,這樣才能做出真正適合sensor的Linux釋出,無疑,工作量是巨大的。

4. 原始資料及參考文獻

[aidcode]   Bootdisk-HOWTO   CDROM-HOWTO   CD-Writing-HOWTO   Distribution-HOWTO   HP-HOWTO   KickStart-HOWTO   Linux-From-Scratch-HOWTO   RedHat-CD-HOWTO   RPM-HOWTO   ~nob/ml/ ... 99902/msg00150.html [/aidcode]

附錄

   附錄A boot.img和initrd.img的解析過程

   1.首先建立兩個映像檔案解析後的裝載點(mount point):

[aidcode]   mkdir /mnt/boot /mnt/initrd [/aidcode]
2.寫shell指令碼進行解析和還原:

   解析指令碼命名為up

[aidcode]   #!/bin/sh   mount -o loop -t msdos boot.img /mnt/boot   gzip -cd /mnt/boot/initrd.img > /tmp/initrd.ext2   mount -o loop –t ext2 /tmp/initrd.ext2 /mnt/initrd [/aidcode]
   還原指令碼命名為down

[aidcode]   #!/bin/sh   umount /mnt/initrd   gzip -c9 /tmp/initrd.ext2 > /mnt/boot/initrd.img   umount /mnt/boot [/aidcode]

   附錄B stage2.img解析過程

   1.首先建立映像檔案解析後的裝載點(mount point):

[aidcode]   mkdir /mnt/stage2 [/aidcode]
   2.寫shell指令碼進行解析和還原:

   解析指令碼命名為up2

[aidcode]   #!/bin/sh   mount -o loop stage2.img /mnt/stage2 [/aidcode]
   還原指令碼命名為down2

[aidcode]   #!/bin/sh   umount /mnt/stage2 [/aidcode]
   附錄C comps檔案清單

[aidcode] 4 1 Base { MAKEDEV SysVinit anacron at basesystem bash bdflush bzip2 chkconfig console-tools cpio cracklib cracklib-dicts crontabs cyrus-sasl openssl db1 db2 db3 dev devfsd diffutils e2fsprogs file filesystem fileutils findutils gawk glib glibc glibc-common grep gzip hdparm hotplug lilo info inits cripts kbdconfig kernel krb5-libs kudzu less libstdc++ libtermcap logrotate losetup mingetty mkbootdisk mkinitrd modutils mount net-tools newt ntsysv pam passwd pciutils popt procps psmisc pwdb quota rpm sed setserial setup setuptool sh-utils shadow-utils slang slocate sysklogd tar termcap textutils time timeconfig tmpwatch util-linux vim-common vim-minimal vixie-cron which zlib } 1 --hide IDS Sensor { iptables iputils libpcap mysql openssh openssh-clients openssl perl rdate snort tcpdump traceroute unzip zip } [/aidcode]
   附錄D hdlist和hdlist2生成方

   假設在/tmp/cdimage/RedHat/RPMS目錄下收錄著RPM包的話,則透過使用 [aidcode]   genhdlist /tmp/cdimage/ [/aidcode]

   生成hdlist和hdlist2檔案,檔案的生成位置在目錄/tmp/cdimage/RedHat/base/下

   附錄E sensor.py檔案清單

[aidcode]   from installclass import BaseInstallClass   from translate import *   from installclass import FSEDIT_CLEAR_ALL   import os   import iutil   class InstallClass(BaseInstallClass):   name = N_("IDS Sensor")   pixmap = "sensor.png"   sortPriority = 10   def __init__(self, expert): BaseInstallClass.__init__(self) self.setGroups(["IDS Sensor"]) self.addToSkipList("lilo") self.addNewPartition('/boot', (48, -1, 0), (None, -1, 0), (0,0)) self.addNewPartition('/', (256, -1, 0), (None, -1, 0), (0,0)) self.addNewPartition('/usr', (512, -1, 1), (None, -1, 0), (0,0)) self.addNewPartition('/var', (256, -1, 0), (None, -1, 0), (0,0)) self.addNewPartition('/home',(512, -1, 1), (None, -1, 0), (0,0)) self.setClearParts(FSEDIT_CLEAR_ALL, warningText = N_("Automatic partitioning will erase ALL DATA on your hard " "drive to make room for your Linux installation.")) # self.addNewPartition('swap', (64, 256, 1), (None, -1, 0), (0,0)) # 2.4 kernel requires more swap, so base amount we try to get # on amount of memory (minswap, maxswap) = iutil.swapSuggestion() self.addNewPartition('swap', (minswap, maxswap, 1), (None, -1, 0), (0,0)) [/aidcode]
   附錄F syslinux.cfg檔案清單

[aidcode]   label linux   kernel vmlinuz   append initrd=initrd.img lang=us devfs=nomount vga=788 [/aidcode]
   附錄G boot.img製作啟動盤方法

[aidcode]   cat boot.img > /dev/fd0 [/aidcode]
   或

[aidcode]   dd if=boot.img of=/dev/fd0 bs=1440 [/aidcode]
   附錄H Linux光碟映像製作方法

   在Linux下光碟映像的製作用mkisofs命令,具體的,假設在/tmp/cdimage/目錄下收錄著將要被製作的光碟內容,則執行

[aidcode]   mkisofs –v –r –T –J –V “NetCop Linux” –b images/boot.img    -o /tmp/NetCopLinux.iso [/aidcode]
   即可在/tmp目錄下做出一命名為NetCopLinux.iso的光碟映像檔案,它以/tmp/cdimage/images/boot.img 作為光碟啟動檔案。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-947118/,如需轉載,請註明出處,否則將追究法律責任。

相關文章