這是一個連載的博文系列,我將持續為大家提供儘可能透徹的Android原始碼分析 github連載地址
前言
上一篇中講到,Linux系統執行完初始化操作最後會執行根目錄下的init檔案,init是一個可執行程式, 它的原始碼在platform/system/core/init/init.cpp。 之前我們講過init程式是使用者空間的第一個程式,我們熟悉的app應用程式都是以它為父程式的, init程式入口函式是main函式,這個函式做的事情還是比較多的,主要分為三個部分
- init程式第一階段
- init程式第二階段
- init.rc檔案解析
由於內容比較多,所以對於init的講解,我分為三個章節來講,本文只講解第一階段,第一階段主要有以下內容
- ueventd/watchdogd跳轉及環境變數設定
- 掛載檔案系統並建立目錄
- 初始化日誌輸出、掛載分割槽裝置
- 啟用SELinux安全策略
- 開始第二階段前的準備
本文涉及到的檔案
platform/system/core/init/init.cpp
platform/system/core/init/ueventd.cpp
platform/system/core/init/watchdogd.cpp
platform/system/core/init/log.cpp
platform/system/core/base/logging.cpp
platform/system/core/init/init_first_stage.cpp
platform/external/selinux/libselinux/src/callbacks.c
platform/external/selinux/libselinux/src/load_policy.c
platform/external/selinux/libselinux/src/getenforce.c
platform/external/selinux/libselinux/src/setenforce.c
platform/external/selinux/libselinux/src/android/android.c
複製程式碼
一、ueventd/watchdogd跳轉及環境變數設定
/*
* 1.C++中主函式有兩個引數,第一個引數argc表示引數個數,第二個引數是引數列表,也就是具體的引數
* 2.init的main函式有兩個其它入口,一是引數中有ueventd,進入ueventd_main,二是引數中有watchdogd,進入watchdogd_main
*/
int main(int argc, char** argv) {
/*
* 1.strcmp是String的一個函式,比較字串,相等返回0
* 2.C++中0也可以表示false
* 3.basename是C庫中的一個函式,得到特定的路徑中的最後一個'/'後面的內容,
* 比如/sdcard/miui_recovery/backup,得到的結果是backup
*/
if (!strcmp(basename(argv[0]), "ueventd")) { //當argv[0]的內容為ueventd時,strcmp的值為0,!strcmp為1
//1表示true,也就執行ueventd_main,ueventd主要是負責裝置節點的建立、許可權設定等一些列工作
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) {//watchdogd俗稱看門狗,用於系統出問題時重啟系統
return watchdogd_main(argc, argv);
}
if (REBOOT_BOOTLOADER_ON_PANIC) {
install_reboot_signal_handlers(); //初始化重啟系統的處理訊號,內部通過sigaction 註冊訊號,當監聽到該訊號時重啟系統
}
add_environment("PATH", _PATH_DEFPATH);//註冊環境變數PATH
//#define _PATH_DEFPATH "/sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin"
複製程式碼
1.1 ueventd_main
定義在platform/system/core/init/ueventd.cpp
Android根檔案系統的映像中不存在“/dev”目錄,該目錄是init程式啟動後動態建立的。
因此,建立Android中裝置節點檔案的重任,也落在了init程式身上。為此,init程式建立子程式ueventd,並將建立裝置節點檔案的工作託付給ueventd。 ueventd通過兩種方式建立裝置節點檔案。
第一種方式對應“冷插拔”(Cold Plug),即以預先定義的裝置資訊為基礎,當ueventd啟動後,統一建立裝置節點檔案。這一類裝置節點檔案也被稱為靜態節點檔案。
第二種方式對應“熱插拔”(Hot Plug),即在系統執行中,當有裝置插入USB埠時,ueventd就會接收到這一事件,為插入的裝置動態建立裝置節點檔案。這一類裝置節點檔案也被稱為動態節點檔案。
int ueventd_main(int argc, char **argv)
{
/*
* init sets the umask to 077 for forked processes. We need to
* create files with exact permissions, without modification by
* the umask.
*/
umask(000); //設定新建檔案的預設值,這個與chmod相反,這裡相當於新建檔案後的許可權為666
/* Prevent fire-and-forget children from becoming zombies.
* If we should need to wait() for some children in the future
* (as opposed to none right now), double-forking here instead
* of ignoring SIGCHLD may be the better solution.
*/
signal(SIGCHLD, SIG_IGN);//忽略子程式終止訊號
InitKernelLogging(argv); //初始化日誌輸出
LOG(INFO) << "ueventd started!";
selinux_callback cb;
cb.func_log = selinux_klog_callback;
selinux_set_callback(SELINUX_CB_LOG, cb);//註冊selinux相關的用於列印log的回撥函式
ueventd_parse_config_file("/ueventd.rc"); //解析.rc檔案,這個後續再講
ueventd_parse_config_file("/vendor/ueventd.rc");
ueventd_parse_config_file("/odm/ueventd.rc");
/*
* keep the current product name base configuration so
* we remain backwards compatible and allow it to override
* everything
* TODO: cleanup platform ueventd.rc to remove vendor specific
* device node entries (b/34968103)
*/
std::string hardware = android::base::GetProperty("ro.hardware", "");
ueventd_parse_config_file(android::base::StringPrintf("/ueventd.%s.rc", hardware.c_str()).c_str());
device_init();//建立一個socket來接收uevent,再對核心啟動時註冊到/sys/下的驅動程式進行“冷插拔”處理,以建立對應的節點檔案。
pollfd ufd;
ufd.events = POLLIN;
ufd.fd = get_device_fd();//獲取device_init中建立出的socket
while (true) {//開戶無限迴圈,隨時監聽驅動
ufd.revents = 0;
int nr = poll(&ufd, 1, -1);//監聽來自驅動的uevent
if (nr <= 0) {
continue;
}
if (ufd.revents & POLLIN) {
handle_device_fd();//驅動程式進行“熱插拔”處理,以建立對應的節點檔案。
}
}
return 0;
}
複製程式碼
1.2 watchdogd_main
定義在platform/system/core/init/watchdogd.cpp
"看門狗"本身是一個定時器電路,內部會不斷的進行計時(或計數)操作,計算機系統和"看門狗"有兩個引腳相連線, 正常執行時每隔一段時間就會通過其中一個引腳向"看門狗"傳送訊號,"看門狗"接收到訊號後會將計時器清零並重新開始計時, 而一旦系統出現問題,進入死迴圈或任何阻塞狀態,不能及時傳送訊號讓"看門狗"的計時器清零,當計時結束時, "看門狗"就會通過另一個引腳向系統傳送“復位訊號”,讓系統重啟
watchdogd_main主要是定時器作用,而DEV_NAME就是那個引腳
int watchdogd_main(int argc, char **argv) {
InitKernelLogging(argv);
int interval = 10;
/*
* C++中atoi作用是將字串轉變為數值
*/
if (argc >= 2) interval = atoi(argv[1]);
int margin = 10;
if (argc >= 3) margin = atoi(argv[2]);
LOG(INFO) << "watchdogd started (interval " << interval << ", margin " << margin << ")!";
int fd = open(DEV_NAME, O_RDWR|O_CLOEXEC); //開啟檔案 /dev/watchdog
if (fd == -1) {
PLOG(ERROR) << "Failed to open " << DEV_NAME;
return 1;
}
int timeout = interval + margin;
/*
* ioctl是裝置驅動程式中對裝置的I/O通道進行管理的函式,WDIOC_SETTIMEOUT是設定超時時間
*/
int ret = ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
if (ret) {
PLOG(ERROR) << "Failed to set timeout to " << timeout;
ret = ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
if (ret) {
PLOG(ERROR) << "Failed to get timeout";
} else {
if (timeout > margin) {
interval = timeout - margin;
} else {
interval = 1;
}
LOG(WARNING) << "Adjusted interval to timeout returned by driver: "
<< "timeout " << timeout
<< ", interval " << interval
<< ", margin " << margin;
}
}
while (true) {//每間隔一定時間往檔案中寫入一個空字元,這就是看門狗的關鍵了
write(fd, "", 1);
sleep(interval);
}
}
複製程式碼
1.3 install_reboot_signal_handlers
定義在platform/system/core/init/init.cpp
這個函式主要作用將各種訊號量,如SIGABRT,SIGBUS等的行為設定為SA_RESTART,一旦監聽到這些訊號即執行重啟系統
static void install_reboot_signal_handlers() {
// Instead of panic'ing the kernel as is the default behavior when init crashes,
// we prefer to reboot to bootloader on development builds, as this will prevent
// boot looping bad configurations and allow both developers and test farms to easily
// recover.
struct sigaction action;
memset(&action, 0, sizeof(action));
sigfillset(&action.sa_mask);//將所有訊號加入至訊號集
action.sa_handler = [](int) {
// panic() reboots to bootloader
panic(); //重啟系統
};
action.sa_flags = SA_RESTART;
sigaction(SIGABRT, &action, nullptr);
sigaction(SIGBUS, &action, nullptr);
sigaction(SIGFPE, &action, nullptr);
sigaction(SIGILL, &action, nullptr);
sigaction(SIGSEGV, &action, nullptr);
#if defined(SIGSTKFLT)
sigaction(SIGSTKFLT, &action, nullptr);
#endif
sigaction(SIGSYS, &action, nullptr);
sigaction(SIGTRAP, &action, nullptr);
}
複製程式碼
1.4 add_environment
定義在platform/system/core/init/init.cpp
這個函式主要作用是將一個鍵值對放到一個Char陣列中,如果陣列中有key就替換,沒有就插入,跟Java中的Map差不多
/* add_environment - add "key=value" to the current environment */
int add_environment(const char *key, const char *val)
{
size_t n;
size_t key_len = strlen(key);
/* The last environment entry is reserved to terminate the list */
for (n = 0; n < (arraysize(ENV) - 1); n++) {
/* Delete any existing entry for this key */
if (ENV[n] != NULL) {
/*
* C++中strcspn用於返回字元所在下標,相當於String的indexof
*/
size_t entry_key_len = strcspn(ENV[n], "=");
if ((entry_key_len == key_len) && (strncmp(ENV[n], key, entry_key_len) == 0)) { //如果key相同,刪除對應資料
free((char*)ENV[n]);
ENV[n] = NULL;
}
}
/* Add entry if a free slot is available */
if (ENV[n] == NULL) { //如果沒有對應key,則插入資料
char* entry;
asprintf(&entry, "%s=%s", key, val);
ENV[n] = entry;
return 0;
}
}
LOG(ERROR) << "No env. room to store: '" << key << "':'" << val << "'";
return -1;
}
複製程式碼
二、 掛載檔案系統並建立目錄
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);//檢視是否有環境變數INIT_SECOND_STAGE
/*
* 1.init的main方法會執行兩次,由is_first_stage控制,first_stage就是第一階段要做的事
*/
if (is_first_stage) {//只執行一次,因為在方法體中有設定INIT_SECOND_STAGE
boot_clock::time_point start_time = boot_clock::now();
// Clear the umask.
umask(0); //清空檔案許可權
// Get the basic filesystem setup we need put together in the initramdisk
// on / and then we'll let the rc file figure out the rest.
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
// Don't expose the raw commandline to unprivileged processes.
chmod("/proc/cmdline", 0440);
gid_t groups[] = { AID_READPROC };
setgroups(arraysize(groups), groups);
mount("sysfs", "/sys", "sysfs", 0, NULL);
mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));
mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));
...
}
...
}
複製程式碼
2.1 mount
mount是用來掛載檔案系統的,mount屬於Linux系統呼叫
int mount(const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data);
複製程式碼
引數:
source:將要掛上的檔案系統,通常是一個裝置名。
target:檔案系統所要掛載的目標目錄。
filesystemtype:檔案系統的型別,可以是"ext2","msdos","proc","ntfs","iso9660"。。。
mountflags:指定檔案系統的讀寫訪問標誌,可能值有以下
引數 | 含義 |
---|---|
MS_BIND | 執行bind掛載,使檔案或者子目錄樹在檔案系統內的另一個點上可視。 |
MS_DIRSYNC | 同步目錄的更新。 |
MS_MANDLOCK | 允許在檔案上執行強制鎖。 |
MS_MOVE | 移動子目錄樹。 |
MS_NOATIME | 不要更新檔案上的訪問時間。 |
MS_NODEV | 不允許訪問裝置檔案。 |
MS_NODIRATIME | 不允許更新目錄上的訪問時間。 |
MS_NOEXEC | 不允許在掛上的檔案系統上執行程式。 |
MS_NOSUID | 執行程式時,不遵照set-user-ID和set-group-ID位。 |
MS_RDONLY | 指定檔案系統為只讀。 |
MS_REMOUNT | 重新載入檔案系統。這允許你改變現存檔案系統的mountflag和資料,而無需使用先解除安裝,再掛上檔案系統的方式。 |
MS_SYNCHRONOUS | 同步檔案的更新。 |
MNT_FORCE | 強制解除安裝,即使檔案系統處於忙狀態。 |
MNT_EXPIRE | 將掛載點標記為過時。 |
data:檔案系統特有的引數
在init初始化過程中,Android分別掛載了tmpfs,devpts,proc,sysfs,selinuxfs這5類檔案系統。
tmpfs是一種虛擬記憶體檔案系統,它會將所有的檔案儲存在虛擬記憶體中, 如果你將tmpfs檔案系統解除安裝後,那麼其下的所有的內容將不復存在。 tmpfs既可以使用RAM,也可以使用交換分割槽,會根據你的實際需要而改變大小。 tmpfs的速度非常驚人,畢竟它是駐留在RAM中的,即使用了交換分割槽,效能仍然非常卓越。 由於tmpfs是駐留在RAM的,因此它的內容是不持久的。 斷電後,tmpfs的內容就消失了,這也是被稱作tmpfs的根本原因。
devpts檔案系統為偽終端提供了一個標準介面,它的標準掛接點是/dev/ pts。 只要pty的主複合裝置/dev/ptmx被開啟,就會在/dev/pts下動態的建立一個新的pty裝置檔案。
proc檔案系統是一個非常重要的虛擬檔案系統,它可以看作是核心內部資料結構的介面, 通過它我們可以獲得系統的資訊,同時也能夠在執行時修改特定的核心引數。
與proc檔案系統類似,sysfs檔案系統也是一個不佔有任何磁碟空間的虛擬檔案系統。 它通常被掛接在/sys目錄下。sysfs檔案系統是Linux2.6核心引入的, 它把連線在系統上的裝置和匯流排組織成為一個分級的檔案,使得它們可以在使用者空間存取
selinuxfs也是虛擬檔案系統,通常掛載在/sys/fs/selinux目錄下,用來存放SELinux安全策略檔案
2.2 mknod
mknod用於建立Linux中的裝置檔案
int mknod(const char* path, mode_t mode, dev_t dev) {
}
複製程式碼
引數: path:裝置所在目錄 mode:指定裝置的型別和讀寫訪問標誌 可能的型別
引數 | 含義 |
---|---|
S_IFMT | type of file ,檔案型別掩碼 |
S_IFREG | regular 普通檔案 |
S_IFBLK | block special 塊裝置檔案 |
S_IFDIR | directory 目錄檔案 |
S_IFCHR | character special 字元裝置檔案 |
S_IFIFO | fifo 管道檔案 |
S_IFNAM | special named file 特殊檔案 |
S_IFLNK | symbolic link 連結檔案 |
dev 表示裝置,由makedev(1, 9) 函式建立,9為主裝置號、1為次裝置號
2.3 其他命令
mkdir也是Linux系統呼叫,作用是建立目錄,第一個引數是目錄路徑,第二個是讀寫許可權
chmod用於修改檔案/目錄的讀寫許可權
setgroups 用來將list 陣列中所標明的組加入到目前程式的組設定中
這裡我解釋下檔案的許可權,也就是類似0755這種,要理解許可權首先要明白「使用者和組」的概念
Linux系統可以有多個使用者,多個使用者可以屬於同一個組,使用者和組的概念就像我們人和家庭一樣,人屬於家庭的一分子,使用者屬於一個組,我們一般在Linux終端輸入ls -al之後會有如下結果
drwxr-xr-x 7 foxleezh foxleezh 4096 2月 24 14:31 .android
複製程式碼
第一個foxleezh表示所有者,這裡的foxleezh表示一個使用者,類似foxleezh這個人
第二個foxleezh表示檔案所有使用者組,這裡的foxleezh表示一個組,類似foxleezh這個家庭
然後我們來看下dwxr-xr-x,這個要分成四部分來理解,d表示目錄(檔案用 - 表示),wxr表示所有者許可權,xr表示檔案所有使用者組的許可權,x表示其他使用者的許可權
- w- 表示寫許可權,用2表示
- x- 表示執行許可權,用1表示
- r- 表示讀取許可權,用4表示 那麼dwxr-xr-x還有種表示方法就是751,是不是感覺跟0755差不多了,那0755前面那個0表示什麼意思呢?
0755前面的0跟suid和guid有關
- suid意味著其他使用者擁有和檔案所有者一樣的許可權,用4表示
- guid意味著其他使用者擁有和檔案所有使用者組一樣的許可權,用2表示
三、 初始化日誌輸出、掛載分割槽裝置
if (is_first_stage) {
...
// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
// talk to the outside world...
InitKernelLogging(argv);
LOG(INFO) << "init first stage started!";
if (!DoFirstStageMount()) {
LOG(ERROR) << "Failed to mount required partitions early ...";
panic();//重啟系統
}
...
}
複製程式碼
3.1 InitKernelLogging
定義在platform/system/core/init/log.cpp
InitKernelLogging首先是將標準輸入輸出重定向到"/sys/fs/selinux/null",然後呼叫InitLogging初始化log日誌系統
void InitKernelLogging(char* argv[]) {
// Make stdin/stdout/stderr all point to /dev/null.
int fd = open("/sys/fs/selinux/null", O_RDWR); //開啟檔案
if (fd == -1) {
int saved_errno = errno;
android::base::InitLogging(argv, &android::base::KernelLogger);
errno = saved_errno;
PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
}
/*
* dup2(int old_fd, int new_fd) 的作用是複製檔案描述符,將old複製到new,下文中將
* 0、1、2繫結到null裝置上,通過標準的輸入輸出無法輸出資訊
*/
dup2(fd, 0); //重定向標準輸入stdin
dup2(fd, 1);//重定向標準輸出stdout
dup2(fd, 2);//重定向標準錯誤stderr
if (fd > 2) close(fd);
android::base::InitLogging(argv, &android::base::KernelLogger);//初始化log
}
複製程式碼
3.2 InitLogging
定義在platform/system/core/base/logging.cpp
InitLogging主要工作是設定logger和aborter的處理函式,然後設定日誌系統輸出等級
void InitLogging(char* argv[], LogFunction&& logger, AbortFunction&& aborter) {
/*
* C++中foo(std::forward<T>(arg))表示將arg按原本的左值或右值,傳遞給foo方法,
LogFunction& 這種表示是左值,LogFunction&&這種表示是右值
*/
SetLogger(std::forward<LogFunction>(logger)); //設定logger處理函式
SetAborter(std::forward<AbortFunction>(aborter));//設定aborter處理函式
if (gInitialized) {
return;
}
gInitialized = true;
// Stash the command line for later use. We can use /proc/self/cmdline on
// Linux to recover this, but we don't have that luxury on the Mac/Windows,
// and there are a couple of argv[0] variants that are commonly used.
if (argv != nullptr) {
std::lock_guard<std::mutex> lock(LoggingLock());
ProgramInvocationName() = basename(argv[0]);
}
const char* tags = getenv("ANDROID_LOG_TAGS");//獲取系統當前日誌輸出等級
if (tags == nullptr) {
return;
}
std::vector<std::string> specs = Split(tags, " "); //將tags以空格拆分成陣列
for (size_t i = 0; i < specs.size(); ++i) {
// "tag-pattern:[vdiwefs]"
std::string spec(specs[i]);
if (spec.size() == 3 && StartsWith(spec, "*:")) { //如果字元數為3且以*:開頭
//那麼根據第三個字元來設定日誌輸出等級(比如*:d,就是DEBUG級別)
switch (spec[2]) {
case 'v':
gMinimumLogSeverity = VERBOSE;
continue;
case 'd':
gMinimumLogSeverity = DEBUG;
continue;
case 'i':
gMinimumLogSeverity = INFO;
continue;
case 'w':
gMinimumLogSeverity = WARNING;
continue;
case 'e':
gMinimumLogSeverity = ERROR;
continue;
case 'f':
gMinimumLogSeverity = FATAL_WITHOUT_ABORT;
continue;
// liblog will even suppress FATAL if you say 's' for silent, but that's
// crazy!
case 's':
gMinimumLogSeverity = FATAL_WITHOUT_ABORT;
continue;
}
}
LOG(FATAL) << "unsupported '" << spec << "' in ANDROID_LOG_TAGS (" << tags
<< ")";
}
}
複製程式碼
3.3 KernelLogger
定義在platform/system/core/base/logging.cpp
在InitKernelLogging方法中有句呼叫
android::base::InitLogging(argv, &android::base::KernelLogger);
複製程式碼
這句的作用就是將KernelLogger函式作為log日誌的處理函式,KernelLogger主要作用就是將要輸出的日誌格式化之後寫入到 /dev/kmsg 裝置中
void KernelLogger(android::base::LogId, android::base::LogSeverity severity,
const char* tag, const char*, unsigned int, const char* msg) {
// clang-format off
static constexpr int kLogSeverityToKernelLogLevel[] = {
[android::base::VERBOSE] = 7, // KERN_DEBUG (there is no verbose kernel log
// level)
[android::base::DEBUG] = 7, // KERN_DEBUG
[android::base::INFO] = 6, // KERN_INFO
[android::base::WARNING] = 4, // KERN_WARNING
[android::base::ERROR] = 3, // KERN_ERROR
[android::base::FATAL_WITHOUT_ABORT] = 2, // KERN_CRIT
[android::base::FATAL] = 2, // KERN_CRIT
};
// clang-format on
static_assert(arraysize(kLogSeverityToKernelLogLevel) == android::base::FATAL + 1,
"Mismatch in size of kLogSeverityToKernelLogLevel and values in LogSeverity");
//static_assert是編譯斷言,如果第一個引數為true,那麼編譯就不通過,這裡是判斷kLogSeverityToKernelLogLevel陣列個數不能大於7
static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC)); //開啟 /dev/kmsg 檔案
if (klog_fd == -1) return;
int level = kLogSeverityToKernelLogLevel[severity];//根據傳入的日誌等級得到Linux的日誌等級,也就是kLogSeverityToKernelLogLevel對應下標的對映
// The kernel's printk buffer is only 1024 bytes.
// TODO: should we automatically break up long lines into multiple lines?
// Or we could log but with something like "..." at the end?
char buf[1024];
size_t size = snprintf(buf, sizeof(buf), "<%d>%s: %s\n", level, tag, msg);//格式化日誌輸出
if (size > sizeof(buf)) {
size = snprintf(buf, sizeof(buf), "<%d>%s: %zu-byte message too long for printk\n",
level, tag, size);
}
iovec iov[1];
iov[0].iov_base = buf;
iov[0].iov_len = size;
TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));//將日誌寫入到 /dev/kmsg 中
}
複製程式碼
3.3 DoFirstStageMount
定義在platform/system/core/init/init_first_stage.cpp
主要作用是初始化特定裝置並掛載
bool DoFirstStageMount() {
// Skips first stage mount if we're in recovery mode.
if (IsRecoveryMode()) { //如果是刷機模式,直接跳過掛載
LOG(INFO) << "First stage mount skipped (recovery mode)";
return true;
}
// Firstly checks if device tree fstab entries are compatible.
if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) { //如果fstab/compatible的值不是android,fstab,直接跳過掛載
LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
return true;
}
std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
if (!handle) {
LOG(ERROR) << "Failed to create FirstStageMount";
return false;
}
return handle->DoFirstStageMount(); //主要是初始化特定裝置並掛載
}
複製程式碼
3.4 handle->DoFirstStageMount
定義在platform/system/core/init/init_first_stage.cpp
這裡主要作用是去解析/proc/device-tree/firmware/android/fstab,然後得到"/system", "/vendor", "/odm"三個目錄的掛載資訊
FirstStageMount::FirstStageMount()
: need_dm_verity_(false), device_tree_fstab_(fs_mgr_read_fstab_dt(), fs_mgr_free_fstab) {
if (!device_tree_fstab_) {
LOG(ERROR) << "Failed to read fstab from device tree";
return;
}
for (auto mount_point : {"/system", "/vendor", "/odm"}) {
fstab_rec* fstab_rec =
fs_mgr_get_entry_for_mount_point(device_tree_fstab_.get(), mount_point); //這裡主要是把掛載的資訊解析出來
if (fstab_rec != nullptr) {
mount_fstab_recs_.push_back(fstab_rec);//將掛載資訊放入陣列中存起來
}
}
}
複製程式碼
四、啟用SELinux安全策略
SELinux是「Security-Enhanced Linux」的簡稱,是美國國家安全域性「NSA=The National Security Agency」 和SCC(Secure Computing Corporation)開發的 Linux的一個擴張強制訪問控制安全模組。 在這種訪問控制體系的限制下,程式只能訪問那些在他的任務中所需要檔案
if (is_first_stage) {
...
//Avb即Android Verfied boot,功能包括Secure Boot, verfying boot 和 dm-verity,
//原理都是對二進位制檔案進行簽名,在系統啟動時進行認證,確保系統執行的是合法的二進位制映象檔案。
//其中認證的範圍涵蓋:bootloader,boot.img,system.img
SetInitAvbVersionInRecovery();//在刷機模式下初始化avb的版本,不是刷機模式直接跳過
// Set up SELinux, loading the SELinux policy.
selinux_initialize(true);//載入SELinux policy,也就是安全策略,
// We're in the kernel domain, so re-exec init to transition to the init domain now
// that the SELinux policy has been loaded.
/*
* 1.這句英文大概意思是,我們執行第一遍時是在kernel domain,所以要重新執行init檔案,切換到init domain,
* 這樣SELinux policy才已經載入進來了
* 2.後面的security_failure函式會呼叫panic重啟系統
*/
if (restorecon("/init") == -1) { //restorecon命令用來恢復SELinux檔案屬性即恢復檔案的安全上下文
PLOG(ERROR) << "restorecon failed";
security_failure(); //失敗則重啟系統
}
...
}
複製程式碼
4.1 selinux_initialize
定義在platform/system/core/init/init.cpp
static void selinux_initialize(bool in_kernel_domain) {
Timer t;
selinux_callback cb;
cb.func_log = selinux_klog_callback;
selinux_set_callback(SELINUX_CB_LOG, cb); //設定selinux的日誌輸出處理函式
cb.func_audit = audit_callback;
selinux_set_callback(SELINUX_CB_AUDIT, cb);//設定selinux的記錄許可權檢測的處理函式
if (in_kernel_domain) {//這裡是分了兩個階段,第一階段in_kernel_domain為true,第二階段為false
LOG(INFO) << "Loading SELinux policy";
if (!selinux_load_policy()) { //載入selinux的安全策略
panic();
}
bool kernel_enforcing = (security_getenforce() == 1); //獲取當前kernel的工作模式
bool is_enforcing = selinux_is_enforcing(); //獲取工作模式的配置
if (kernel_enforcing != is_enforcing) { //如果當前的工作模式與配置的不同,就將當前的工作模式改掉
if (security_setenforce(is_enforcing)) {
PLOG(ERROR) << "security_setenforce(%s) failed" << (is_enforcing ? "true" : "false");
security_failure();
}
}
if (!write_file("/sys/fs/selinux/checkreqprot", "0")) {
security_failure();
}
// init's first stage can't set properties, so pass the time to the second stage.
setenv("INIT_SELINUX_TOOK", std::to_string(t.duration_ms()).c_str(), 1);
} else {
selinux_init_all_handles(); //第二階段時初始化處理函式
}
}
複製程式碼
4.2 selinux_set_callback
定義在platform/external/selinux/libselinux/src/callbacks.c
主要就是根據不同的type設定回撥函式,selinux_log,selinux_audit這些都是函式指標
void selinux_set_callback(int type, union selinux_callback cb)
{
switch (type) {
case SELINUX_CB_LOG:
selinux_log = cb.func_log;
break;
case SELINUX_CB_AUDIT:
selinux_audit = cb.func_audit;
break;
case SELINUX_CB_VALIDATE:
selinux_validate = cb.func_validate;
break;
case SELINUX_CB_SETENFORCE:
selinux_netlink_setenforce = cb.func_setenforce;
break;
case SELINUX_CB_POLICYLOAD:
selinux_netlink_policyload = cb.func_policyload;
break;
}
}
複製程式碼
4.3 selinux_load_policy
定義在platform/system/core/init/init.cpp
這裡區分了兩種情況,這兩種情況只是區分從哪裡載入安全策略檔案,第一個是從 /vendor/etc/selinux/precompiled_sepolicy 讀取 ,第二個是從 /sepolicy 讀取,他們最終都是呼叫selinux_android_load_policy_from_fd方法
static bool selinux_load_policy() {
return selinux_is_split_policy_device() ? selinux_load_split_policy()
: selinux_load_monolithic_policy();
}
複製程式碼
4.4 selinux_android_load_policy_from_fd
定義在platform/external/selinux/libselinux/src/android/android.c
這個函式主要作用是設定selinux_mnt 的值為/sys/fs/selinux ,然後呼叫security_load_policy
int selinux_android_load_policy_from_fd(int fd, const char *description)
{
int rc;
struct stat sb;
void *map = NULL;
static int load_successful = 0;
/*
* Since updating policy at runtime has been abolished
* we just check whether a policy has been loaded before
* and return if this is the case.
* There is no point in reloading policy.
*/
if (load_successful){
selinux_log(SELINUX_WARNING, "SELinux: Attempted reload of SELinux policy!/n");
return 0;
}
set_selinuxmnt(SELINUXMNT); //SELINUXMNT的值為 /sys/fs/selinux
if (fstat(fd, &sb) < 0) {
selinux_log(SELINUX_ERROR, "SELinux: Could not stat %s: %s\n",
description, strerror(errno));
return -1;
}
/*
* mmap 的作用是將一個檔案或者其它物件對映進記憶體
*/
map = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) {
selinux_log(SELINUX_ERROR, "SELinux: Could not map %s: %s\n",
description, strerror(errno));
return -1;
}
rc = security_load_policy(map, sb.st_size);
if (rc < 0) {
selinux_log(SELINUX_ERROR, "SELinux: Could not load policy: %s\n",
strerror(errno));
munmap(map, sb.st_size);
return -1;
}
munmap(map, sb.st_size);
selinux_log(SELINUX_INFO, "SELinux: Loaded policy from %s\n", description);
load_successful = 1;
return 0;
}
複製程式碼
4.5 security_load_policy
定義在platform/external/selinux/libselinux/src/load_policy.c
這個函式主要作用就是寫入data到/sys/fs/selinux,data其實就是之前找的那些策略檔案,由此我們知道,看起來selinux_load_policy呼叫這麼多程式碼, 其實只是將策略檔案拷貝到 /sys/fs/selinux 目錄下
int security_load_policy(void *data, size_t len)
{
char path[PATH_MAX];
int fd, ret;
if (!selinux_mnt) { //selinux_mnt的值為 /sys/fs/selinux
errno = ENOENT;
return -1;
}
snprintf(path, sizeof path, "%s/load", selinux_mnt);
fd = open(path, O_RDWR); //開啟 /sys/fs/selinux ,然後將data的值寫入
if (fd < 0)
return -1;
ret = write(fd, data, len);
close(fd);
if (ret < 0)
return -1;
return 0;
}
複製程式碼
4.6 security_setenforce
定義在platform/external/selinux/libselinux/src/setenforce.c
selinux有兩種工作模式:
- permissive,所有的操作都被允許(即沒有MAC),但是如果違反許可權的話,會記錄日誌,一般eng模式用
- enforcing,所有操作都會進行許可權檢查。一般user和user-debug模式用
不管是security_setenforce還是security_getenforce都是去操作/sys/fs/selinux/enforce 檔案, 0表示permissive 1表示enforcing
int security_setenforce(int value)
{
int fd, ret;
char path[PATH_MAX];
char buf[20];
if (!selinux_mnt) {
errno = ENOENT;
return -1;
}
snprintf(path, sizeof path, "%s/enforce", selinux_mnt);
fd = open(path, O_RDWR); //開啟 /sys/fs/selinux/enforce 檔案
if (fd < 0)
return -1;
snprintf(buf, sizeof buf, "%d", value);
ret = write(fd, buf, strlen(buf)); //將value的值寫入檔案
close(fd);
if (ret < 0)
return -1;
return 0;
}
複製程式碼
五、開始第二階段前的準備
這裡主要就是設定一些變數如INIT_SECOND_STAGE,INIT_STARTED_AT,為第二階段做準備,然後再次呼叫init的main函式,啟動使用者態的init程式
if (is_first_stage) {
...
setenv("INIT_SECOND_STAGE", "true", 1);
static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
setenv("INIT_STARTED_AT", StringPrintf("%" PRIu64, start_ms).c_str(), 1);//記錄第二階段開始時間戳
char* path = argv[0];
char* args[] = { path, nullptr };
execv(path, args); //重新執行main方法,進入第二階段
// execv() only returns if an error happened, in which case we
// panic and never fall through this conditional.
PLOG(ERROR) << "execv(\"" << path << "\") failed";
security_failure();
}
複製程式碼
小結
init程式第一階段做的主要工作是掛載分割槽,建立裝置節點和一些關鍵目錄,初始化日誌輸出系統,啟用SELinux安全策略
下一篇我將講解init程式第二階段