Android情景分析之屬性服務
我們都知道,在Windows平臺上有一個登錄檔管理器,登錄檔的內容採用key-value鍵值對的形式來記錄使用者、軟體的一些使用資訊。即使系統或者軟體重啟,它還是能夠根據之前在登錄檔中的記錄,進行相應的初始化工作。
那麼在Android平臺上,也有類似的機制,稱之為屬性服務(property service)。應用程式可以通過這個屬性機制,查詢或者設定相應的屬性。我們可以使用getprop命令來檢視當前系統中都有哪些屬性。比如我的紅米手機,如圖1所示(圖中只顯示了部分屬性)。
圖1
建立屬性內容儲存檔案
屬性服務的初始化工作主要是在init程序的main函式中完成。程式碼路徑system\core\init\init.c。init的main函式中首先呼叫property_init()函式建立儲存檔案,程式碼如下:
void property_init(void) { init_property_area(); } static int init_property_area(void) { // 判斷是否已經初始化過了 if (property_area_inited) return -1; // 開啟/dev/__properties__裝置,然後mmap一塊大小為(128 * 1024)的記憶體出來 if(__system_property_area_init()) return -1; /* pa_workspace是個全域性變數,結構如下所示 typedef struct { size_t size; 共享記憶體的大小 int fd; 共享記憶體的檔案描述符 } workspace; init_workspace函式開啟裝置/dev/__properties__,將檔案描述符賦值給fd,size清零。 */ if(init_workspace(&pa_workspace, 0)) return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC); // 初始化完畢 property_area_inited = 1; return 0; }
再來看看__system_property_area_init函式具體做了什麼事情,__system_property_area_init->map_prop_area_rw:
static int map_prop_area_rw() { prop_area *pa; int fd; int ret; /* #define PROP_FILENAME "/dev/__properties__" static char property_filename[PATH_MAX] = PROP_FILENAME; 這裡就是開啟/dev/__properties__檔案,感情property area是用共享檔案來實現的 指定了O_CREAT標誌,如果檔案不存在則建立之 */ fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444); if (fd < 0) { if (errno == EACCES) { /* for consistency with the case where the process has already * mapped the page in and segfaults when trying to write to it */ abort(); } return -1; } ret = fcntl(fd, F_SETFD, FD_CLOEXEC); if (ret < 0) goto out; /* #define PA_SIZE (128 * 1024) 改變檔案大小為PA_SZIE */ if (ftruncate(fd, PA_SIZE) < 0) goto out; /* 並不是整個pa都用來記錄屬性內容的,那麼其前面sizeof(prop_area)的大小是用來記錄這個pa的一些基本資訊,例如,特徵值、版本號等等。緊接著prop_area結構後面的data記憶體才是真正用來記錄屬性資訊的。 struct prop_area { unsigned bytes_used; unsigned volatile serial; unsigned magic; unsigned version; unsigned reserved[28]; char data[0]; }; */ pa_size = PA_SIZE; pa_data_size = pa_size - sizeof(prop_area); compat_mode = false; /* 在init程序中將/dev/__properties__檔案對映成一塊可讀可寫的記憶體 注意,我們在後面會看到,在其他客戶端程序都是以只讀的形式訪問pa的,如果客戶端要修改或者新增屬性,那麼需要程序間通訊,最後都是由init程序來完成的 */ pa = mmap(NULL, pa_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(pa == MAP_FAILED) goto out; memset(pa, 0, pa_size); pa->magic = PROP_AREA_MAGIC; pa->version = PROP_AREA_VERSION; /* reserve root node */ pa->bytes_used = sizeof(prop_bt); /* plug into the lib property services 最後將pa複製給全域性變數__system_property_area__ */ __system_property_area__ = pa; close(fd); return 0; out: close(fd); return -1; }
上面的內容比較簡單,不過最後的一個賦值語句__system_property_area__ = pa;比較特殊。__system_property_area__變數是在bioniclibc庫中定義的一個全域性變數。這裡為什麼要賦值給它呢?
原來,雖然屬性區域是由init程序在初始化的時候建立的,但是Android希望其他程序也能讀取這塊記憶體裡面的東西,為了做到這一點,它便做了如下兩項工作:
1. 屬性區域通過檔案形式實現程序間共享資料。
2. 如何讓其他程序也去讀取這個檔案呢?Android利用了gcc的constructor屬性,這個屬性指明瞭一個__libc_prenit函式,當bionic libc庫被載入時,將自動呼叫這個__libc_prenit,這個函式內部就將完成共享檔案到本地程序的對映工作。
客戶端程序對映屬性檔案
直接看程式碼吧,bionic\libc\bionic\libc_init_dynamic.cpp
// constructor屬性指示載入器載入該庫後,首先呼叫__libc_preinit函式。這一點和windows
// 上動態庫的DllMain函式類似
__attribute__((constructor)) static void __libc_preinit() {
…
__libc_init_common(*args);
…
}
void __libc_init_common(KernelArgumentBlock& args) {
…
__system_properties_init(); // 初始化話客戶端屬性區域.
}
__system_properties_init –> map_prop_area
static int map_prop_area()
{
bool fromFile = true;
int result = -1;
int fd;
int ret;
// 開啟已經存在的檔案/dev/__properties__
fd = open(property_filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd >= 0) {
/* For old kernels that don't support O_CLOEXEC */
ret = fcntl(fd, F_SETFD, FD_CLOEXEC);
if (ret < 0)
goto cleanup;
}
if ((fd < 0) && (errno == ENOENT)) {
fd = get_fd_from_env();
fromFile = false;
}
if (fd < 0) {
return -1;
}
// 讀取並檢查檔案屬性
struct stat fd_stat;
if (fstat(fd, &fd_stat) < 0) {
goto cleanup;
}
if ((fd_stat.st_uid != 0)
|| (fd_stat.st_gid != 0)
|| ((fd_stat.st_mode & (S_IWGRP | S_IWOTH)) != 0)
|| (fd_stat.st_size < sizeof(prop_area)) ) {
goto cleanup;
}
// 客戶端取得共享檔案的大小
pa_size = fd_stat.st_size;
pa_data_size = pa_size - sizeof(prop_area);
// mmap將檔案對映為記憶體,此處為PROT_READ形式,客戶端程序對屬性檔案是隻讀// 的,而不能對其進行設定
prop_area *pa = mmap(NULL, pa_size, PROT_READ, MAP_SHARED, fd, 0);
if (pa == MAP_FAILED) {
goto cleanup;
}
// 檢查屬性區域的特徵
if((pa->magic != PROP_AREA_MAGIC) || (pa->version != PROP_AREA_VERSION &&
pa->version != PROP_AREA_VERSION_COMPAT)) {
munmap(pa, pa_size);
goto cleanup;
}
if (pa->version == PROP_AREA_VERSION_COMPAT) {
compat_mode = true;
}
result = 0;
// 賦值到本地全域性變數
__system_property_area__ = pa;
cleanup:
if (fromFile) {
close(fd);
}
return result;
}
載入屬性檔案並啟動屬性服務
接著看init程序的初始化程式碼
// 如果啟動模式不是charger,那麼就載入/default.prop屬性檔案
if (!is_charger)
property_load_boot_defaults();
最後呼叫queue_builtin_action(property_service_init_action,"property_service_init");完成屬性伺服器的啟動過程。
property_service_init_action-> start_property_service:
void start_property_service(void)
{
int fd;
/*
#define PROP_PATH_SYSTEM_BUILD "/system/build.prop"
#define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop"
*/
load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);
// 如果是除錯模式,就載入
// #define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop"
load_override_properties();
/*
Read persistent properties after all default values have been loaded.
有一些屬性是需要儲存到永久介質上的,這些屬性檔案則由下面這個函式載入,這些檔案儲存在/data/property目錄下,並且這些檔案的檔名必須以persist.開頭。
*/
load_persistent_properties();
// 建立一個socket,用於ipc通訊
fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
if(fd < 0) return;
fcntl(fd, F_SETFD, FD_CLOEXEC);
fcntl(fd, F_SETFL, O_NONBLOCK);
// 開始監聽
listen(fd, 8);
// 賦值給全域性變數
property_set_fd = fd;
}
init程序處理設定屬性請求
設定屬性的請求都是在init程序的main函式的一個for迴圈中進行處理的。下面我們繼續看程式碼:
for (i = 0; i < fd_count; i++) {
if (ufds[i].revents == POLLIN) {
if (ufds[i].fd == get_property_set_fd())
handle_property_set_fd();
else if (ufds[i].fd == get_keychord_fd())
handle_keychord();
else if (ufds[i].fd == get_signal_fd())
handle_signal();
}
}
上面start_property_service函式中建立了一個用於通訊的socket,最後賦值給全域性變數property_set_fd,此處就是判斷收到的資訊是否是是屬性伺服器的socket。
int get_property_set_fd()
{
return property_set_fd;
}
如果是屬性伺服器的通訊socket,那麼就呼叫handle_property_set_fd函式來處理訊息,並設定相應的屬性。
void handle_property_set_fd()
{
prop_msg msg;
int s;
int r;
int res;
struct ucred cr;
struct sockaddr_un addr;
socklen_t addr_size = sizeof(addr);
socklen_t cr_size = sizeof(cr);
char * source_ctx = NULL;
// 接收TCP連線
if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) {
return;
}
/*
Check socket options here
取出socket的可選內容,可能包括客戶端程序的許可權等屬性
*/
if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
close(s);
ERROR("Unable to receive socket options\n");
return;
}
// 接收socket的主體資料
r = TEMP_FAILURE_RETRY(recv(s, &msg, sizeof(msg), 0));
if(r != sizeof(prop_msg)) {
ERROR("sys_prop: mis-match msg size received: %d expected: %d errno: %d\n",
r, sizeof(prop_msg), errno);
close(s);
return;
}
switch(msg.cmd) {
case PROP_MSG_SETPROP:
msg.name[PROP_NAME_MAX-1] = 0;
msg.value[PROP_VALUE_MAX-1] = 0;
// 檢查屬性名,不能有特殊字元,或者兩個點..這樣的名字
if (!is_legal_property_name(msg.name, strlen(msg.name))) {
ERROR("sys_prop: illegal property name. Got: \"%s\"\n", msg.name);
close(s);
return;
}
getpeercon(s, &source_ctx);
/*
如果是ctl開頭的訊息,則認為是控制訊息,控制訊息用來執行一些命令,例如用adb shell登入後,輸入setprop ctl.start bootanim就可以檢視開機動畫了,如果要關閉就輸入setprop.stop bootanim就可以了。
*/
if(memcmp(msg.name,"ctl.",4) == 0) {
close(s);
if (check_control_perms(msg.value, cr.uid, cr.gid, source_ctx)) {
handle_control_message((char*) msg.name + 4, (char*) msg.value);
} else {
ERROR("sys_prop: Unable to %s service ctl [%s] uid:%d gid:%d pid:%d\n",
msg.name + 4, msg.value, cr.uid, cr.gid, cr.pid);
}
} else {
/*
不是ctl開頭的屬性,則首先檢查其許可權。例如,設定net.開頭的屬性需要AID_SYSTEM許可權,log.開頭的屬性需要AID_SHELL屬性等。
*/
if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {
// 最後通過property_set函式設定客戶端需要設定的屬性
property_set((char*) msg.name, (char*) msg.value);
} else {
ERROR("sys_prop: permission denied uid:%d name:%s\n",
cr.uid, msg.name);
}
// Note: bionic's property client code assumes that the
// property server will not close the socket until *AFTER*
// the property is written to memory.
close(s);
}
freecon(source_ctx);
break;
default:
close(s);
break;
}
}
當客戶端的許可權滿足要求時,init程序就呼叫property_set進行相關處理,這個函式邏輯比較簡單,程式碼如下所示:
int property_set(const char *name, const char *value)
{
prop_info *pi;
int ret;
size_t namelen = strlen(name);
size_t valuelen = strlen(value);
// 檢查屬性名的合法性
if (!is_legal_property_name(name, namelen)) return -1;
if (valuelen >= PROP_VALUE_MAX) return -1;
// 根據屬性名查詢是否已經存在
pi = (prop_info*) __system_property_find(name);
if(pi != 0) {
/* 如果是ro.開頭的屬性表明是隻讀的,直接返回*/
if(!strncmp(name, "ro.", 3)) return -1;
// 否則更新相應屬性的值
__system_property_update(pi, value, valuelen);
} else {
// 如果還沒有該屬性,那麼就新增一項
ret = __system_property_add(name, namelen, value, valuelen);
if (ret < 0) {
ERROR("Failed to set '%s'='%s'\n", name, value);
return ret;
}
}
/* If name starts with "net." treat as a DNS property. */
if (strncmp("net.", name, strlen("net.")) == 0) {
if (strcmp("net.change", name) == 0) {
return 0;
}
/*
* The 'net.change' property is a special property used track when any
* 'net.*' property name is updated. It is _ONLY_ updated here. Its value
* contains the last updated 'net.*' property.
*/
property_set("net.change", name);
} else if (persistent_properties_loaded &&
strncmp("persist.", name, strlen("persist.")) == 0) {
// 如果屬性名是以persist.開頭的,表明該屬性是永久的,那麼需要把屬性寫
// 到相應檔案中去
write_persistent_property(name, value);
} else if (strcmp("selinux.reload_policy", name) == 0 &&
strcmp("1", value) == 0) {
// 與selinux相關
selinux_reload_policy();
}
/*
init.rc中有如下這句話:
on property:persist.service.adb.enable=1
start adbd
待persist.service.adb.enable屬性設定為1後,就執行start adbd這個command,這就是通過property_changed函式來完成的。
*/
property_changed(name, value);
return 0;
}
property_changed函式其主要實現如下,property_changed-> queue_property_triggers:
void queue_property_triggers(const char *name, const char *value)
{
struct listnode *node;
struct action *act;
list_for_each(node, &action_list) {
act = node_to_item(node, struct action, alist);
if (!strncmp(act->name, "property:", strlen("property:"))) {
const char *test = act->name + strlen("property:");
int name_length = strlen(name);
if (!strncmp(name, test, name_length) &&
test[name_length] == '=' &&
(!strcmp(test + name_length + 1, value) ||
!strcmp(test + name_length + 1, "*"))) {
action_add_queue_tail(act);
}
}
}
}
客戶端設定屬性
客戶端通過property_set傳送設定屬性的請求,程式碼如下所示:property_set -> __system_property_set
int __system_property_set(const char *key, const char *value)
{
int err;
prop_msg msg;
if(key == 0) return -1;
if(value == 0) value = "";
if(strlen(key) >= PROP_NAME_MAX) return -1;
if(strlen(value) >= PROP_VALUE_MAX) return -1;
memset(&msg, 0, sizeof msg);
msg.cmd = PROP_MSG_SETPROP;
strlcpy(msg.name, key, sizeof msg.name);
strlcpy(msg.value, value, sizeof msg.value);
err = send_prop_msg(&msg);
if(err < 0) {
return err;
}
return 0;
}
static int send_prop_msg(prop_msg *msg)
{
struct pollfd pollfds[1];
struct sockaddr_un addr;
socklen_t alen;
size_t namelen;
int s;
int r;
int result = -1;
s = socket(AF_LOCAL, SOCK_STREAM, 0);
if(s < 0) {
return result;
}
memset(&addr, 0, sizeof(addr));
// #define PROP_SERVICE_NAME "property_service"
// static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME;
namelen = strlen(property_service_socket);
strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path);
addr.sun_family = AF_LOCAL;
alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1;
// 會連線上init程序裡面的socket
if(TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) {
close(s);
return result;
}
r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0));
if(r == sizeof(prop_msg)) {
pollfds[0].fd = s;
pollfds[0].events = 0;
r = TEMP_FAILURE_RETRY(poll(pollfds, 1, 250 /* ms */));
if (r == 1 && (pollfds[0].revents & POLLHUP) != 0) {
result = 0;
} else {
result = 0;
}
}
close(s);
return result;
}