嵌入式Linux——應用除錯:自制系統呼叫,並編寫程序檢視器
簡介:
本文主要講解在ARM Linux中系統呼叫的原理,並根據這些原理在系統中新增自制的系統呼叫函式,最後我們還將通過自制的系統呼叫函式來檢視應用程式指定位置的資訊,用此方法實現應用程式的除錯。
Linux核心:linux-2.6.22.6
所用開發板:JZ2440 V3(S3C2440A)
C庫 :glibc-2.3.6
宣告:
本文主要是看完韋東山老師的視訊後所寫,同時文中會引用一些網友文章中的觀點來完善這方面的知識。我會將參考的文章在該文章的末尾標出。希望我的文章會對你有所幫助。
系統呼叫:
在計算機中,系統呼叫(英語:system call)指執行在使用者空間的程式向作業系統核心請求需要更高許可權執行的服務。系統呼叫提供使用者程式與作業系統之間的介面。大多數系統互動式操作需求在核心態執行。如裝置IO操作或者程序間通訊。
我們用下圖進行說明:
從上圖中可以看出,應用程式可以通過系統呼叫介面來訪問核心空間,同時也可以呼叫C庫中的函式來訪問系統呼叫介面然後呼叫核心中的函式。而本文主要介紹後面一種方式。我們在應用程式中使用open,read,write函式,然後在C庫中將open,read,write函式解析為swi指令加對應的立即數。而不同的立即數對應不同的函式。而swi為軟體中斷,swi指令會導致CPU異常,然後進入核心異常處理模式。在核心異常處理模式中會保護在使用者模式的現場,然後進入核心態在核心中根據導致異常的swi指令,並從中取出立即數找到open,read,write函式對應的sys_open,sys_read,sys_write函式。然後在這些函式中完成我們想要完成的事情。從而實現系統呼叫。
我們在上面大致的描述了系統呼叫的過程,下面我們用程式碼描述這個過程。我們在glibc中搜索swi會發現在sysdeps\unix\sysv\linux\arm\brk.c中找到關於swi的指令語句:
int __brk (void *addr) { void *newbrk; asm ("mov a1, %1\n" /* save the argment in r0 */ "swi %2\n" /* do the system call */ "mov %0, a1;" /* keep the return value */ : "=r"(newbrk) : "r"(addr), "i" (SYS_ify (brk)) : "a1"); __curbrk = newbrk; if (newbrk < addr) { __set_errno (ENOMEM); return -1; } return 0; }
而上面程式碼在C語言中加有彙編語句,而其中就有swi指令。我們在分析這個彙編指令之前先要對這個彙編指令的格式有一定的瞭解。我們首先要介紹的是上面彙編程式碼中的三個‘:’,第一個‘:’後面表示的是要輸出的引數,第二個‘:’後面表示的是輸入引數,而第三個‘:’後面表示的是會變更的引數。而從第一個‘:’開始依次往下引數遞增,並分別用%0,%1·····表示。而當面的字母‘r’表示的是暫存器,‘i’表示立即數。下面我們就可以分析這個函數了,在這個彙編程式碼中有mov a1, %1其中的%1就是下面的輸入引數addr,所以翻譯過來就是mov a1,addr將addr的值放入a1暫存器中。而swi %2中的%2為(SYS_ify (brk)),這要看SYS_ify 的巨集定義了:
#undef SYS_ify
#define SWI_BASE (0x900000)
#define SYS_ify(syscall_name) (__NR_##syscall_name)
在上面的巨集中##表示連詞符,所以他的意思為將__NR_與後面SYS_ify中的字串連線起來。而在本例中為__NR_brk。而我們在核心程式碼中找到:
#define __NR_brk (__NR_SYSCALL_BASE+ 45)
而__NR_SYSCALL_BASE的定義為
#define __NR_OABI_SYSCALL_BASE 0x900000
#define __NR_SYSCALL_BASE __NR_OABI_SYSCALL_BASE
所以回到上面的彙編指令:swi %2其實就是swi 900045 。而我們在核心的arch\arm\kernel\calls.S檔案中看到第45個函式就是sys_brk。
而在核心中sys_brk的函式原型為:
asmlinkage unsigned long sys_brk(unsigned long brk)
{······
}
瞭解了這些我們再看核心中對於swi指令是如何反應的,即在核心中如何根據swi中的值確定呼叫那個函式。我們先看韋東山老師書中的圖:
上圖中列出了異常向量和他們對應的處理函式我們看程式碼中的異常向量為:
.align 5
.LCvswi:
.word vector_swi
.globl __stubs_end
__stubs_end:
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0
b vector_und + stubs_offset
ldr pc, .LCvswi + stubs_offset
b vector_pabt + stubs_offset
b vector_dabt + stubs_offset
b vector_addrexcptn + stubs_offset
b vector_irq + stubs_offset
b vector_fiq + stubs_offset
.globl __vectors_end
__vectors_end:
上面列出了各種異常的異常向量。我們這裡主要看swi的異常為:
ldr pc, .LCvswi + stubs_offset
而LCvswi的定義為:
.LCvswi:
.word vector_swi
所以我們找vector_swi所對應的函式為(我將不重要的部分刪除):
ENTRY(vector_swi) /* 首先我們進入異常前要儲存現場 */
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0 - r12
add r8, sp, #S_PC
stmdb r8, {sp, lr}^ @ Calling sp, lr
mrs r8, spsr @ called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
zero_fp
/*
* Get the system call number.
*/
ldr scno, [lr, #-4] @ get SWI instruction獲得swi指令,其中scno就是(system call number的縮寫)
A710( and ip, scno, #0x0f000000 @ check for SWI ) @檢測是否是swi指令
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
enable_irq /* 使能中斷 */
adr tbl, sys_call_table @ 將系統呼叫表放入到tbl中
cmp scno, #NR_syscalls @ 檢測系統呼叫是否超出最大範圍
adr lr, ret_fast_syscall @ 設定系統呼叫後的返回地址
ldrcc pc, [tbl, scno, lsl #2] @ 進入系統呼叫函式
上面程式碼已經說明了系統呼叫的過程。我們這裡總結一下為:
1. 首先我們進入異常前要儲存現場
2. 獲得swi指令
3. 將系統呼叫表放入到tbl中
4. 檢測系統呼叫是否超出最大範圍
5. 設定系統呼叫後的返回地址
6. 進入系統呼叫函式
我們接下來對上面的一些知識點進行說明,首先是
and ip, scno, #0x0f000000
我們為什麼通過上面的比較就能確定這是不是一個swi指令那?那我們就要去2440中看一下swi的命令格式了。
從上面看出,swi指令的第24位到第27位全為1,所以用0x0f000000來判斷他是否為swi指令。
而系統呼叫表我們就要看後面:
.type sys_call_table, #object
ENTRY(sys_call_table)
#include "calls.S"
#undef ABI
#undef OBSOLETE
從上面看,系統呼叫表其實就是包含在arch\arm\kernel\calls.S檔案中的各種呼叫函式:
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork_wrapper)
CALL(sys_read)
CALL(sys_write)
/* 5 */ CALL(sys_open)
CALL(sys_close)
············
CALL(sys_signalfd)
/* 350 */ CALL(sys_timerfd)
CALL(sys_eventfd)
在上面的檔案中列出了各種系統呼叫的函式。我們上面的sys_call_table中存放的就是這些函式,而我們的scno就對應著這些呼叫函式。我們現在看CALL(x)的定義。
.equ NR_syscalls,0
#define CALL(x) .equ NR_syscalls,NR_syscalls+1
#include "calls.S"
#undef CALL
#define CALL(x) .long x
從上面可以看出CALL(x)其實就是.equ NR_syscalls,NR_syscalls+1的巨集定義,而他的意思是NR_syscalls自加一。也就是說我們程式中有多少個CALL(x)就可以用NR_syscalls表示。所以NR_syscalls為系統中系統呼叫函式的總和。這也可以解釋我們為什麼要用NR_syscalls檢測scno是否超出最大範圍:
cmp scno, #NR_syscalls @ 檢測系統呼叫是否超出最大範圍
之後我們就要呼叫系統呼叫函數了 ,我們這裡以write函式為例。我們看如果他想實現函式呼叫要做哪些事。
首先我們一定要在arch\arm\kernel\calls.S中加入write的CALL定義。來確保我們在上面彙編語句中能夠找到有write這個選項。
接著我們就要真正的定義這個函數了,我們在fs\read_write.c中定義這個函式為:
asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
file = fget_light(fd, &fput_needed);
if (file) {
loff_t pos = file_pos_read(file);
ret = vfs_write(file, buf, count, &pos);
file_pos_write(file, pos);
fput_light(file, fput_needed);
}
return ret;
}
函式定義完之後我們最後就是宣告這個函數了,我們在include\linux\syscalls.h中宣告這個函式:
asmlinkage ssize_t sys_write(unsigned int fd, const char __user *buf,
size_t count);
完成了這些我們就可以使用系統呼叫來呼叫這個函數了。
自制系統呼叫:
我們根據上面的介紹來寫自制的系統呼叫。這裡我們自制一個hello的系統呼叫。我們按著上面介紹write的步驟寫這個系統呼叫。
首先,我們在arch\arm\kernel\calls.S的末尾加上CALL(sys_hello),由於他為第352個CALL定義,所以他為352號。
然後我們去fs\read_write.c中模仿sys_write函式寫sys_hello函式:
asmlinkage void sys_hello(char __user * buf, size_t count)
{
char ker_buf[100];
if(buf){
copy_from_user(ker_buf,buf,count<100 ? count:100);
ker_buf[99] = '\0';
printk("sys_hello: %s \n",ker_buf);
}
}
EXPORT_SYMBOL_GPL(sys_hello);
最後,我們去include\linux\syscalls.h中宣告這個函式:
asmlinkage ssize_t sys_write(unsigned int fd, const char __user *buf,
size_t count);
做完上面的事,我們就完成了自制的系統呼叫。現在我們就可以測試這個系統呼叫了。下面是測試應用程式:
#include <errno.h>
#include <unistd.h>
#define __NR_SYSCALL_BASE 0x900000
void hello(char *buf,int count)
{
/* swi */
asm("mov r0 ,%0 \n"
"mov r1 ,%1 \n"
"swi %2 \n"
:
:"r"(buf),"r"(count),"i"(__NR_SYSCALL_BASE + 352)
:"r0","r1"
);
}
int main(int argc,char **argv)
{
printf("in app,call hello \n");
hello("jia", 12);
return 0;
}
使用自制系統呼叫編寫程序檢視器:
這裡我們就要介紹對上面這個自制系統呼叫的使用了,我們將使用這個系統呼叫在我們的應用程式中打斷點,然後將應用程式中一些重要的資訊輸出。下面簡要的說明一下這個過程:
1. 修改應用的可執行檔案,替換其中某個位置的機器碼為我們編寫的系統呼叫的機器碼。
2. 執行這個可執行檔案,當執行到這個指定的位置時,進入系統呼叫函式
3. 在系統呼叫函式中列印資訊,最後補上我們代替的機器碼的指令
4. 從系統呼叫中返回,執行原來的指令
下面我列出一個簡單的測試應用程式:
#include <stdio.h>
#include <unistd.h>
int cnt = 0;
void C(void)
{
int i = 0;
while(1){
printf("hello,cnt = %d, i = %d\n",cnt,i);
cnt++;
i += 2; //在這裡打斷點
sleep(2);
}
}
void B(void)
{
C();
}
void A(void)
{
B();
}
int main(int argc,char **argv)
{
A();
return 0;
}
我們在上面程式的C函式中i += 2;處打斷點,也就是說我們要在這個測試程式的可執行檔案中用swi指令的機器碼代替這句話的機器碼。而為什麼我們要選擇代替i += 2;而不是其他的語句?是因為這個語句簡單,我們要在後面補上這條語句,所以他要越簡單越容易替補。同時如果是複雜的語句可能要多條機器碼,而我們的swi只替代一條。
我們將上面的程式編譯獲得可執行檔案,並使用命令:arm-linux-objdump -D test_sc > test_sc.dis 獲得測試程式的反彙編檔案。在反彙編檔案中找到i += 2;所對應的彙編語句為:
00008490 <C>:
8490: e1a0c00d mov ip, sp
8494: e92dd800 stmdb sp!, {fp, ip, lr, pc}
8498: e24cb004 sub fp, ip, #4 ; 0x4
849c: e24dd004 sub sp, sp, #4 ; 0x4
84a0: e3a03000 mov r3, #0 ; 0x0
84a4: e50b3010 str r3, [fp, #-16]
84a8: e59f3030 ldr r3, [pc, #48] ; 84e0 <.text+0x144>
84ac: e59f0030 ldr r0, [pc, #48] ; 84e4 <.text+0x148>
84b0: e5931000 ldr r1, [r3]
84b4: e51b2010 ldr r2, [fp, #-16]
84b8: ebffffb4 bl 8390 <.text-0xc>
84bc: e59f201c ldr r2, [pc, #28] ; 84e0 <.text+0x144>
84c0: e59f3018 ldr r3, [pc, #24] ; 84e0 <.text+0x144>
84c4: e5933000 ldr r3, [r3]
84c8: e2833001 add r3, r3, #1 ; 0x1
84cc: e5823000 str r3, [r2]
84d0: e51b3010 ldr r3, [fp, #-16]
84d4: e2833002 add r3, r3, #2 ; 0x2 ,該條語句為i += 2;的彙編程式碼
84d8: e50b3010 str r3, [fp, #-16]
84dc: eafffff1 b 84a8 <C+0x18>
84e0: 00010788 andeq r0, r1, r8, lsl #15
84e4: 00008650 andeq r8, r0, r0, asr r6
e1a0c00d mov ip, sp
8494: e92dd800 stmdb sp!, {fp, ip, lr, pc}
8498: e24cb004 sub fp, ip, #4 ; 0x4
849c: e24dd004 sub sp, sp, #4 ; 0x4
84a0: e3a03000 mov r3, #0 ; 0x0
84a4: e50b3010 str r3, [fp, #-16]
84a8: e59f3030 ldr r3, [pc, #48] ; 84e0 <.text+0x144>
84ac: e59f0030 ldr r0, [pc, #48] ; 84e4 <.text+0x148>
84b0: e5931000 ldr r1, [r3]
84b4: e51b2010 ldr r2, [fp, #-16]
84b8: ebffffb4 bl 8390 <.text-0xc>
84bc: e59f201c ldr r2, [pc, #28] ; 84e0 <.text+0x144>
84c0: e59f3018 ldr r3, [pc, #24] ; 84e0 <.text+0x144>
84c4: e5933000 ldr r3, [r3]
84c8: e2833001 add r3, r3, #1 ; 0x1
84cc: e5823000 str r3, [r2]
84d0: e51b3010 ldr r3, [fp, #-16]
84d4: e2833002 add r3, r3, #2 ; 0x2 ,該條語句為i += 2;的彙編程式碼
84d8: e50b3010 str r3, [fp, #-16]
84dc: eafffff1 b 84a8 <C+0x18>
84e0: 00010788 andeq r0, r1, r8, lsl #15
84e4: 00008650 andeq r8, r0, r0, asr r6
從上面看:
84d4: e2833002 add r3, r3, #2 ; 0x2 ,該條語句為i += 2;的彙編程式碼
為i += 2;的彙編程式碼,而這條語句的機器碼為e2833002,因此我們要到可執行檔案中找: 02 30 83 e2 的機器碼。然後用swi指令替換,那麼我們swi的機器碼為多少那?這個就要看一下我們前面一個測試程式的反彙編檔案了:
00008490 <hello>:
8490: e1a0c00d mov ip, sp
8494: e92dd800 stmdb sp!, {fp, ip, lr, pc}
8498: e24cb004 sub fp, ip, #4 ; 0x4
849c: e24dd008 sub sp, sp, #8 ; 0x8
84a0: e50b0010 str r0, [fp, #-16]
84a4: e50b1014 str r1, [fp, #-20]
84a8: e51b2010 ldr r2, [fp, #-16]
84ac: e51b3014 ldr r3, [fp, #-20]
84b0: e1a00002 mov r0, r2
84b4: e1a01003 mov r1, r3
84b8: ef900160 swi 0x00900160 ;swi語句
84bc: e24bd00c sub sp, fp, #12 ; 0xc
84c0: e89da800 ldmia sp, {fp, sp, pc}
從上面看他的swi指令的彙編程式碼為:
84b8: ef900160 swi 0x00900160 ;swi語句
所以他的機器碼為:ef900160,其中的第24位到27位為1表示為swi機器碼,而0x900000表示ARM的系統呼叫基址,而0x160就是352,即sys_hello的函式排序。因此我們要用機器碼:60 01 90 ef 程式碼可執行檔案中的 02 30 83 e2 。改完這個我們就可以去sys_hello函式中編寫程式碼來獲得我們想要的資訊了。例如我們將sys_hello函式改為:
asmlinkage void sys_hello(char __user * buf, size_t count)
{
int val;
struct pt_regs *regs;
/* 1.輸出一些除錯資訊 */
/* 這裡我們輸出應用程式中的cnt值,在反彙編檔案test_sc.dis中搜cnt的cnt的地址為0x00010788 */
copy_from_user(&val, (const void __user *)0x000107c4,4);
printk("sys_hello : cnt = %d \n",val);
/* 2. 執行被替代的指令 */
regs = task_pt_regs(current);
regs->ARM_r3 += 2;
/* 獲得應用程式中C函式區域性變數i的值 */
copy_from_user(&val,(const void __user *)(regs->ARM_fp - 16),4);
printk("sys_hello : i = %d \n",val);
/* 3. 返回 */
}
我們從上面的程式中可以獲得應用程式中全域性變數cnt的值和C函式中區域性變數i的值。我們下面介紹獲得這兩個值的方法。
首先我們介紹獲得cnt值的方法,我們從反彙編檔案test_sc.dis中搜cnt,從而得到cnt的地址為0x00010788
然後我們使用copy_from_user從使用者空間獲得cnt的值。並將它存入val中。
下面我們介紹獲得了C函式中區域性變數i的值的方法。我們從反彙編檔案test_sc.dis中知道i的值是在i += 2;的彙編程式碼前來確定他所在暫存器的位置。所以我們看彙編程式碼,i的值存放在[fp, #-16]的位置。
而我們通過巨集:task_pt_regs獲得程序的暫存器資訊。
#define task_pt_regs(p) \
((struct pt_regs *)(THREAD_START_SP + task_stack_page(p)) - 1)
所以我們使用程式碼:task_pt_regs(current);獲得當前程序的暫存器資訊。而當前程序就是發生swi前應用程式的程序。所以i的值可以通過copy_from_user(&val,(const void __user *)(regs->ARM_fp - 16),4);獲得並將i值賦給val。
而上面的regs->ARM_r3 += 2;語句就是我們替換的語句i += 2;的替補,因為在彙編中他的程式碼為:add r3, r3, #2 ; 即r3暫存器中的值自加2,所以對應的C語句為:regs->ARM_r3 += 2;
通過上面修改我們就可以在系統呼叫中獲得應用程式的資訊了。
參考文章: