مرحبا العالم التحليل

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_stdout, str, len) //... } 

رميت كل القمامة حول التحدي المهم لنا. الآن _IO_sputn هو _IO_sputn الحالي في سلسلة مكالمات hello world. نجد تعريفًا ، هذا الاسم هو ماكرو معرف في libio/libioP.h والذي يستدعي ماكرو آخر ، والذي مرة أخرى ... تحتوي شجرة الماكرو على الشخص التالي:

  #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n) //... #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N) //... #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) //... # define _IO_JUMPS_FUNC(THIS) \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS ((struct _IO_FILE_plus *) (THIS)) + (THIS)->_vtable_offset)) //... #define _IO_JUMPS(THIS) (THIS)->vtable 

ماذا بحق الجحيم يحدث هنا؟ دعنا نوسع كل وحدات الماكرو للنظر في الكود النهائي:

  ((*(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; }; //... struct _IO_jump_t { //... JUMP_FIELD(_IO_xsputn_t, __xsputn); //... JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); //... }; 

إذا _IO_2_1_stdout_ أعمق ، _IO_2_1_stdout_ أن الجدول _IO_2_1_stdout_ تهيئته في الملف libio/stdfiles.c ، ويتم تحديد التطبيقات libio/stdfiles.c لوظائف الجدول في libio/fileops.c :

 /* from libio/stdfiles.c */ DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); /* from libio/fileops.c */ # define _IO_new_file_xsputn _IO_file_xsputn //... const struct _IO_jump_t _IO_file_jumps = { //... JUMP_INIT(xsputn, _IO_file_xsputn), //... JUMP_INIT(read, _IO_file_read), JUMP_INIT(write, _IO_new_file_write), JUMP_INIT(seek, _IO_file_seek), JUMP_INIT(close, _IO_file_close), JUMP_INIT(stat, _IO_file_stat), //... }; 

كل هذا يعني أننا إذا استخدمنا جدول القفز المرتبط بـ 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 (f->_fileno, data, to_do)); // .. } 

أخيرًا ، وظيفة تستدعي شيئًا لا يبدأ بتسطير أسفل السطر! وظيفة 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; //... ret = file->f_op->write(file, buf, count, pos); //... return 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_driver->driver_name = "pty_slave"; pts_driver->name = "pts"; pts_driver->major = UNIX98_PTY_SLAVE_MAJOR; pts_driver->minor_start = 0; pts_driver->type = TTY_DRIVER_TYPE_PTY; pts_driver->subtype = PTY_TYPE_SLAVE; //... tty_set_operations(pts_driver, &pty_unix98_ops); //... /* Now create the /dev/ptmx special device */ tty_default_fops(&ptmx_fops); ptmx_fops.open = ptmx_open; cdev_init(&ptmx_cdev, &ptmx_fops); //... } static const struct tty_operations pty_unix98_ops = { //... .open = pty_open, .close = pty_close, .write = pty_write, //... }; 

عند الكتابة إلى 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) { /* Stuff the data into the input queue of the other end */ c = tty_insert_flip_string(to->port, buf, c); /* And shovel */ 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 

يبدو وكأنه تمثال نصفي قليلا لهذه العملية البسيطة. من الجيد أن يراها فقط أولئك الذين يريدونها حقًا.

Source: https://habr.com/ru/post/ar438044/


All Articles