Hello World هو أحد البرامج الأولى التي نكتبها بأي لغة برمجة.
بالنسبة لـ C ، يبدو عالم الترحيب بسيطًا وقصيرًا:
#include <stdio.h> void main() { printf("Hello World!\n"); }
نظرًا لأن البرنامج قصير جدًا ، يجب أن يكون من الضروري شرح ما يحدث "تحت الغطاء".
أولاً ، دعونا نرى ما يحدث عند التجميع والربط:
gcc --save-temps hello.c -o hello
--save-temps
بحيث يترك gcc
hello.s
، ملف رمز التجميع.
فيما يلي نموذج مجمع الشفرة الذي تلقيته:
.file "hello.c" .section .rodata .LC0: .string "Hello World!" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp movl $.LC0, %edi call puts popq %rbp ret
كما ترون من قائمة المجمّع ، فإنه ليس
printf
يسمى ، ولكن
puts
. يتم أيضًا تعريف وظيفة
puts
في ملف
stdio.h
وتلتزم بطباعة سطر وكسر فاصل.
حسنًا ، لقد فهمنا الوظيفة التي تستدعيها الكود. ولكن أين يتم
puts
تنفذ؟
لتحديد أي مكتبة تطبق ، نستخدم
ldd
، الذي يعرض تبعيات المكتبة ، و
nm
، والذي يعرض أحرف ملف الكائن.
$ ldd hello libc.so.6 => /lib64/libc.so.6 (0x0000003e4da00000) $ nm /lib64/libc.so.6 | grep " puts" 0000003e4da6dd50 W puts
توجد الوظيفة في مكتبة تسمى
libc
وتقع في
/lib64/libc.so.6
على
/lib64/libc.so.6
(Fedora 19). في حالتي ، يعد
/lib64
على
/usr/lib64
، و
/lib64
هو
/lib64
على
/usr/lib64
. هذا الملف يحتوي على جميع الوظائف.
اكتشفنا إصدار
libc
عن طريق تشغيل الملف كما لو كان قابلاً للتنفيذ:
$ /usr/lib64/libc-2.17.so GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al. ...
نتيجة لذلك ، يستدعي برنامجنا وظيفة
puts
من
glibc
الإصدار 2.17. دعونا الآن نرى ما تقوم به وظيفة
puts
في
glibc-2.17
.
يصعب التنقل برمز glibc بسبب الاستخدام الواسع النطاق لوحدات ما قبل المعالجة والبرامج النصية. بالنظر إلى الكود ، نرى ما يلي في
libio/ioputs.c
:
weak_alias (_IO_puts, puts)
في glibc ، هذا يعني أنه عند استدعاء المكالمات ، يتم استدعاء
_IO_puts
بالفعل. تم توضيح هذه الوظيفة في نفس الملف ، ويبدو الجزء الرئيسي من هذه الوظيفة كما يلي:
int _IO_puts (str) const char *str; {
رميت كل القمامة حول التحدي المهم لنا. الآن
_IO_sputn
هو
_IO_sputn
الحالي في سلسلة مكالمات hello world. نجد تعريفًا ، هذا الاسم هو ماكرو معرف في
libio/libioP.h
والذي يستدعي ماكرو آخر ، والذي مرة أخرى ... تحتوي شجرة الماكرو على الشخص التالي:
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
ماذا بحق الجحيم يحدث هنا؟ دعنا نوسع كل وحدات الماكرو للنظر في الكود النهائي:
((*(struct _IO_jump_t **) ((void *) &((struct _IO_FILE_plus *) (((_IO_FILE*)(&_IO_2_1_stdout_)) ) )->vtable+(((_IO_FILE*)(&_IO_2_1_stdout_)) )->_vtable_offset))->__xsputn ) (((_IO_FILE*)(&_IO_2_1_stdout_)), str, len)
عيون تؤذي. اسمحوا لي أن أشرح ما يحدث هنا. يستخدم Glibc جدول القفز لاستدعاء وظائف. في حالتنا ، يقع الجدول في بنية تسمى
_IO_2_1_stdout_
، وتسمى الوظيفة التي نحتاج إليها
__xsputn
.
تم التصريح عن البنية في ملف
libio/libio.h
:
extern struct _IO_FILE_plus _IO_2_1_stdout_;
وفي الملف
libio/libioP.h
تعريفات للهيكل والجدول
libio/libioP.h
:
struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };
إذا
_IO_2_1_stdout_
أعمق ،
_IO_2_1_stdout_
أن الجدول
_IO_2_1_stdout_
تهيئته في الملف
libio/stdfiles.c
، ويتم تحديد التطبيقات
libio/stdfiles.c
لوظائف الجدول في
libio/fileops.c
:
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); # define _IO_new_file_xsputn _IO_file_xsputn
كل هذا يعني أننا إذا استخدمنا جدول القفز المرتبط بـ
stdout
، فسنقوم في النهاية باستدعاء الدالة
_IO_new_file_xsputn
. بالفعل أقرب ، أليس كذلك؟ هذه الوظيفة تلقي البيانات في المخازن المؤقتة وتدعو
new_do_write
عندما يمكن إخراج محتويات المخزن المؤقت. هذا ما يبدو عليه
new_do_write
:
static _IO_size_t new_do_write (fp, data, to_do) _IO_FILE *fp; const char *data; _IO_size_t to_do; { _IO_size_t count; .. count = _IO_SYSWRITE (fp, data, to_do); .. return count; }
بالطبع ، الماكرو يسمى. من خلال نفس آلية الانتقال
__xsputn
لـ
__xsputn
، يتم
__write
. بالنسبة لملفات
__write
،
__write
_IO_new_file_write
إلى
_IO_new_file_write
. وتسمى هذه الوظيفة في نهاية المطاف. دعنا ننظر لها؟
_IO_ssize_t _IO_new_file_write (f, data, n) _IO_FILE *f; const void *data; _IO_ssize_t n; { _IO_ssize_t to_do = n; _IO_ssize_t count = 0; while (to_do > 0) {
أخيرًا ، وظيفة تستدعي شيئًا لا يبدأ بتسطير أسفل السطر! وظيفة
write
معروفة ومحددة في
unistd.h
. هذه طريقة قياسية إلى حد ما لكتابة بايت إلى ملف باستخدام واصف ملف. يتم تعريف وظيفة
write
في glibc نفسها ، لذلك نحن بحاجة إلى العثور على الكود.
لقد وجدت رمز
write
في
sysdeps/unix/syscalls.list
. يتم إنشاء معظم مكالمات النظام ملفوفة في glibc من هذه الملفات. يحتوي الملف على اسم الوظيفة والوسيطات التي تتطلبها. يتم إنشاء نص الدالة من نمط استدعاء نظام شائع.
# File name Caller Syscall name Args Strong name Weak names ... write - write Ci:ibn __libc_write __write write ...
عند استدعاء رمز glibc
write
(إما
__libcwrite
أو
__write
) ، يحدث syscall في kernel. رمز Kernel أكثر قابلية للقراءة من glibc. نقطة الإدخال إلى syscall
write
في
fs/readwrite.c
:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) { struct fd f = fdget(fd); ssize_t ret = -EBADF; if (f.file) { loff_t pos = file_pos_read(f.file); ret = vfs_write(f.file, buf, count, &pos); if (ret >= 0) file_pos_write(f.file, pos); fdput(f); } return ret; }
أولاً ، تم العثور على البنية المتوافقة مع واصف الملف ، ثم يتم
vfs_write
الدالة
vfs_write
من النظام الفرعي لنظام الملفات الظاهري (vfs). سيتوافق الهيكل في حالتنا مع ملف
stdout
. ألقِ نظرة على
vfs_write
:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { ssize_t ret;
تفوض الوظيفة تنفيذ وظيفة
write
الخاصة بملف معين. في نظام Linux ، غالبًا ما يتم تطبيق ذلك في رمز برنامج التشغيل ، لذلك تحتاج إلى معرفة برنامج التشغيل الذي يسمى في حالتنا.
أستخدم Fedora 19 مع Gnome 3. لإجراء التجارب ، ويعني هذا على وجه الخصوص أن الجهاز الطرفي هو
gnome-terminal
افتراضيًا. قم بتشغيل هذا الجهاز ونفذ ما يلي:
~$ tty /dev/pts/0 ~$ ls -l /proc/self/fd total 0 lrwx------ 1 kos kos 64 okt. 15 06:37 0 -> /dev/pts/0 lrwx------ 1 kos kos 64 okt. 15 06:37 1 -> /dev/pts/0 lrwx------ 1 kos kos 64 okt. 15 06:37 2 -> /dev/pts/0 ~$ ls -la /dev/pts total 0 drwxr-xr-x 2 root root 0 okt. 10 10:14 . drwxr-xr-x 21 root root 3580 okt. 15 06:21 .. crw--w---- 1 kos tty 136, 0 okt. 15 06:43 0 c--------- 1 root root 5, 2 okt. 10 10:14 ptmx
يقوم الأمر
tty
بطباعة اسم الملف المرتبط بالإدخال القياسي ، وكما ترون من قائمة الملفات في
/proc
، يرتبط الملف نفسه
/proc
ودفق الأخطاء. تسمى ملفات الجهاز الموجودة في
/dev/pts
زائفة ، وبصورة أكثر دقة ، فهي محطات زائفة للرقيق. عندما تكتب عملية ما إلى محطة زائفة للرقيق ، تنتقل البيانات إلى الوحدة الزائفة الرئيسية. ماجستير محطة الزائفة هو جهاز
/dev/ptmx
.
يوجد برنامج تشغيل الجهاز الزائف في Linux kernel في
drivers/tty/pty.c
:
static void __init unix98_pty_init(void) {
عند الكتابة إلى
pts
، يتم
pty_write
، والذي يبدو كما يلي:
static int pty_write(struct tty_struct *tty, const unsigned char *buf, int c) { struct tty_struct *to = tty->link; if (tty->stopped) return 0; if (c > 0) { c = tty_insert_flip_string(to->port, buf, c); if (c) { tty_flip_buffer_push(to->port); tty_wakeup(tty); } } return c; }
تساعد التعليقات في فهم أن البيانات في قائمة انتظار الإدخال لجهاز الكمبيوتر الطرفي الرئيسي المزيف. لكن من يقرأ من هذا الخط؟
~$ lsof | grep ptmx gnome-ter 13177 kos 11u CHR 5,2 0t0 1133 /dev/ptmx gdbus 13177 13178 kos 11u CHR 5,2 0t0 1133 /dev/ptmx dconf 13177 13179 kos 11u CHR 5,2 0t0 1133 /dev/ptmx gmain 13177 13182 kos 11u CHR 5,2 0t0 1133 /dev/ptmx ~$ ps 13177 PID TTY STAT TIME COMMAND 13177 ? Sl 0:04 /usr/libexec/gnome-terminal-server
gnome-terminal-server
عملية
gnome-terminal-server
all جميع
gnome-terminal
وتخلق محطات زائفة جديدة. هو الذي يستمع إلى محطة الزائفة الرئيسية ، وفي النهاية ، سيتلقى بياناتنا ، وهي
"Hello World"
. يستقبل خادم
gnome-terminal
السلسلة ويعرضها على الشاشة. بشكل عام ، لم يكن هناك ما يكفي من الوقت لتحليل مفصل
gnome-terminal
:)
الخاتمة
المسار العام لخط "Hello World" الخاص بنا:
0. hello: printf("Hello World") 1. glibc: puts() 2. glibc: _IO_puts() 3. glibc: _IO_new_file_xsputn() 4. glibc: new_do_write() 5. glibc: _IO_new_file_write() 6. glibc: syscall write 7. kernel: vfs_write() 8. kernel: pty_write() 9. gnome_terminal: read() 10. gnome_terminal: show to user
يبدو وكأنه تمثال نصفي
قليلا لهذه العملية البسيطة. من الجيد أن يراها فقط أولئك الذين يريدونها حقًا.