-
Notifications
You must be signed in to change notification settings - Fork 19
/
Chapter_II-10.tex
705 lines (583 loc) · 28.3 KB
/
Chapter_II-10.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
\chapter{إدخال نصّ بشكل أكثر أمانا}
إدخال النصوص في لغة \textenglish{C}
هي من أكثر الأمور حساسية. أنت تعرف الدالة
\InlineCode{scanf}
التي تعرّفنا عليها في الفصول الأولى. ستقول: وأيّ الأدوات ستكون أكثر سهولة وطبيعية منها؟ لكن جهّز نفسك، بعد هذا الفصل ستقول عنها أي شيء باستثناء "بسيطة".
الذين سيستعملون برنامجك هم بطبيعة الحال بشر. فهناك منهم من يخطئ في كتابة شيء، بينما هناك من يتعمّدون إرباك برنامجك بمعلومات غير منتظرة. فإن طلبت من المستعمل: ما هو عُمرك؟ من يضمن لك بأنه لن يجيبك بـ:" اسمي فلان وأنا من البلد فلان"؟
الهدف من هذا الفصل هو تعريفك إلى بعض المشاكل التي يمكن أن نواجهها أثناء استعمالنا للدالة
\InlineCode{scanf}،
و تقديم دالة بديلة أكثر أمانًا وهي
\InlineCode{fgets}.
\section{حدود الدالة \texttt{scanf}}
هذه الدالة التي نستعملها جميعًا من الفصول الأولى في الكتاب، هي سلاح ذو حدين:
\begin{itemize}
\item سهلة الاستعمال حينما نكون في مستوى "مبتدئ" ، ولهذا السبب عرّفتك بها.
\item لكن الطريقة التي تعمل بها معقّدة ويمكن أن تكون خطيرة في بعض الحالات.
\end{itemize}
ألا يبدو الأمر متناقضا؟ فإن الدالة
\InlineCode{scanf}
سهلة الاستعمال وفي نفس الوقت أكثر تعقيدًا مما نتصور، سأريك الحدود التي يمكن لهذه الدالة أن تصل إليها وذلك بتقديم مثالين واقعيين.
\subsection{إدخال سلسلة محارف تحتوي على فراغات }
لنفرض أننا طلبنا من المستعمل أن يقوم بإدخال سلسلة محارف في الكونسول، وهو يقوم بكتابة فراغ في سلسلته:
\begin{Csource}
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char name[20] = {0};
printf("What's your name ? ");
scanf("%s", name);
printf("Ah ! Your name is %s !\n\n", name);
return 0;
}
\end{Csource}
\begin{Console}
What's your name ? Mathieu Nebra
Ah ! Your name is Mathieu !
\end{Console}
\begin{question}
لماذا اختفت الكلمة
"\textenglish{Nebra}"
؟
\end{question}
ذلك لأن الدالة
\InlineCode{scanf}
تتوقف عن القراءة حينما تصل إلى فراغ، أو رجوع إلى السطر أو محرف جدولة
(\textenglish{tabulation}).
يعني أنك غير قادر على قراءة سلسلة محرفيّة تحتوي على فراغات.
\begin{information}
في الواقع، الكلمة
"\textenglish{Nebra}"
لازالت مخزّنة في الذاكرة، في شيء نسميه بالمتغير المؤقّت
(\textenglish{buffer})،
المرة القادمة عندما نستدعي الدالة
\InlineCode{scanf}
فهي ستقوم بقراءة الكلمة
"\textenglish{Nebra}"
وحدها الموجودة في المتغير المؤقت.
\end{information}
يمكننا استعمال الدالة
\InlineCode{scanf}
بشكل يسمح لها بقراءة الفراغات، لكن الأمر معقّد جدّا. لمن يصرّ على ذلك، يمكنك إيجاد دروس مفصّلة على الويب، مثل الدرس الأجنبي المتوفّر على هذا الرابط:
\url{http://xrenault.developpez.com/tutoriels/c/scanf/}
\subsection{إدخال سلسلة محارف طويلة للغاية}
يوجد مشكل آخر، أكثر خطورة، وهو
\textbf{تجاوز الذاكرة}.
في الشفرة التي رأيناها، يوجد السطر التالي:
\begin{Csource}
char name[5] = {0};
\end{Csource}
ترى أنني قمت بحجز 5 خانات من أجل الجدول المسمّى
\InlineCode{name}
الذي هو من نوع
\InlineCode{char}.
يعني أننا قادرون على تخزين كلمة من 4 محارف، بينما الحرف الأخير فهو محجوز لعلامة نهاية السلسلة
\InlineCode{\textbackslash 0}.\\
إذا نسيت كلّ هذا فراجع فصل السلاسل المحرفية.
المخطط التالي يمثل المكان الذي هو محجوز للكلمة التي عرّفناها:
\begin{figure}[H]
\centering
\includegraphics[width=0.5\textwidth]{Chapter_II-10_array}
\end{figure}
ماذا لو كتبنا عددا كبيرا من المحارف بالنسبة للمساحة المتوقّعة لتخزين المتغير؟
\begin{Console}
What's your name ? Patrice
Ah ! Your name is Patrice !
\end{Console}
ستقول أن كل شيء على ما يرام لكن الواقع أنك بصدد مواجهة أكبر كابوس لدى المبرمجين!
لقد قمنا بـ\emph{تجاوز في الذاكرة}،
هذا ما نسميه بـ\emph{\textenglish{buffer overflow}}
بالإنجليزية.
كما ترى في المخطط التالي، لقد حجزت 5 خانات لكي تقوم باستعمال 8، ما الذي قامت به الدالة
\InlineCode{scanf}؟
لقد قامت بمواصلة الكتابة في الذاكرة وكأن شيئًا لم يحدث! فلقد استغلّت خانات ليس لها الحق في الكتابة فيها.
\begin{figure}[H]
\centering
\includegraphics[width=0.5\textwidth]{Chapter_II-10_array_patrice}
\end{figure}
الذي جرى في الحقيقة، هو أن المحارف الزائدة تسببت في مسح معلومات من الذاكرة واستبدالها بهذه المحارف. هذا ما نسميه بالـ\emph{\textenglish{buffer overflow}}.
\begin{figure}[H]
\centering
\includegraphics[width=0.5\textwidth]{Chapter_II-10_array_overflow}
\end{figure}
\begin{question}
لما الأمر خطير؟
\end{question}
دون الدخول في التفاصيل، لأنه بإمكاننا البدء في محادثة قدر 50 صفحة ولا نتوقف أبدًا، فلنقل بأنه إن لم يقم البرنامج بمعالجة حالات كهذه، فالمستعمل سيقوم بكتابة ما يحلو له وتخريب المعلومات المتواجدة في الخانات التالية من الذاكرة. أي أنه قادر على كتابة شفرة في تلك الخانات وبرنامجك سيقوم بتشغيل تلك الشفرات وكأنها تابعة له، وهذا ما نسميه بالهجوم عبر المتغير المؤقت
(\textenglish{Buffer overflow attack})،
نوع من الهجومات المعروفة عند القراصنة، ولكنه صعب التحقيق.\\
إذا كنت مهتمًا بهذا الموضوع، يمكنك قراءة المقال التالي من ويكيبيديا (حذار، إنّه مع ذلك معقد جدا):
\url{http://fr.wikipedia.org/wiki/D%C3%A9passement_de_tampon}
الهدف من هذا الفصل هو تأمين قراءة البيانات وذلك بمنع المستعمل من تجاوز الذاكرة وإحداث
\textenglish{buffer overflow}.
بالطبع كان بإمكاننا تعريف جدول كبير للغاية (10,000 خانة) لكن هذا لا يحلّ المشكل فالشخص الذي يريد الوصول إلى الذاكرة ما عليه سوى إدخال سلسلة يتجاوز طولها 10,000 محرف وسيعمل هجومه كما يريد.
الشيء المحزن هو أن معظم المبرمجين لا ينتبهون دائما لهذه الأخطاء، ولو أنهم قاموا بكتابة الشفرة من المرة الأولى بشكل نظيف وصحيح، لما ظهرت كثير من الثغرات التي نتحدّث عنها اليوم.
\section{استرجاع سلسلة محارف}
توجد العديد من الدوال القياسيّة في لغة
\textenglish{C}
التي تسمح باسترجاع سلسلة نصّيّة. إضافة إلى الدالة
\InlineCode{scanf}،
و التي من الصعب دراستها هنا، لدينا:
\begin{itemize}
\item \InlineCode{gets}:
دالة تقرأ سلسلة محرفيّة كاملة لكنها خطيرة جدّا لأنها لا تعالج مشكل الـ\textenglish{buffer overflow}.
\item \InlineCode{fgets}:
تشبه الدالة
\InlineCode{gets}
لكنها تحمي البرنامج وذلك بالتحكم في عدد المحارف المكتوبة في الذاكرة.
\end{itemize}
أعتقد أن الأمر مفهوم: على الرغم من أنها دالّة قياسيّة في \textenglish{C}،
\InlineCode{gets}
هي دالّة خطيرة جدّا. كل البرامج الّتي تستخدمها عرضة لأن يكونوا ضحايا الـ\textenglish{buffer overflow}.
سنرى كيف تعمل الدالة
\InlineCode{fgets}،
و كيف نستعملها في برامجنا الخاصّة في مكان الدالة
\InlineCode{scanf}.
\subsection{الدالّة \texttt{fgets}}
نموذج هذه الدالة، المتواجد في المكتبة
\InlineCode{stdio.h}
هو:
\begin{Csource}
char *fgets(char *str, int num, FILE *stream);
\end{Csource}
من المهم أن نفهم هذا النموذج. معاملات الدالة هي التالية:
\begin{itemize}
\item \InlineCode{str}:
مؤشّر نحو جدول في الذاكرة، أين ستتمكن الدالة من كتابة النص المدخل من طرف المستخدم.
\item \InlineCode{num}:
حجم الجدول
\InlineCode{str}
المرسل كمعامل أوّل.
لاحظ أنه لو قمت بحجز جدول من
\InlineCode{char}،
فإن الدالة
\InlineCode{fgets}
ستقرأ 9 محارف على الأكثر (آخر خانة محجوزة للمحرف
\InlineCode{\textbackslash 0}
المشير إلى نهاية السلسلة).
\item \InlineCode{stream}:
مؤشّر نحو الملف الذي سنقرأ منه. في حالتنا "الملف المراد قراءته" هو الإدخال القياسي
(\textenglish{Standard input})،
أي لوحة المفاتيح. لطلب قراءة الإدخال القياسي نرسل المؤشّر
\InlineCode{stdin}
المعرّف تلقائيّا في الملفات الرأسيّة للمكتبة القياسيّة لـ\textenglish{C}
ليشير إلى لوحة المفاتيح. مع ذلك، يمكن استخدام
\InlineCode{fgets}
لقراءة الملفّات، كما رأينا في الفصل الخاص بالملفّات.
\end{itemize}
الدالة ستقوم بإرجاع نفس المؤشّر
\InlineCode{str}
للإشارة إلى إن كانت القراءة قد تمت بشكل صحيح أم لا. يكفي إذا أن نختبر ما إن كانت قيمة هذا المؤشّر تساوي
\InlineCode{NULL}،
فإن كانت كذلك، فهناك خطأ.
فلنجرّب!
\begin{Csource}
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char name[10];
printf("What's your name ? ");
fgets(name, 10, stdin);
printf("Ah ! Your name is %s !\n\n", name);
return 0;
}
\end{Csource}
\begin{Console}
What's your name ? NEBRA
Ah ! Your name is NEBRA
!
\end{Console}
الدالة تعمل بشكل جيد، مع تفصيل بسيط: عندما تضغط على زر الإدخال، تقوم
\InlineCode{fgets}
بالاحتفاظ بـ\InlineCode{\textbackslash n}
الموافق، هذا ما يفسّر الرجوع إلى السطر بعد الكلمة
"\textenglish{NEBRA}"
كما يظهر في الكونسول.
لا يمكننا أن نمنع هذه الدالة من كتابة المحرف
\InlineCode{\textbackslash n}
لأن الدالة تعمل هكذا. بالمقابل، هذا لا يمنع كتابتنا لدالة خاصّة بالإدخال تقوم نفسها باستدعاء
\InlineCode{fgets}
و حذف ذلك المحرف!
\subsection{كتابة دالتك الخاصّة بالإدخال باستخدام \texttt{fgets}}
ليس صعبًا جدّا أن نقوم بكتابة دالة خاصة بك تقوم ببعض التصحيحات من أجلك في كلّ مرّة.
سنسمّي هذه الدالّة
\InlineCode{read}.
ستقوم بإرجاع القيمة 0 إن كان هناك خطأ و1 إن لم يكن.
\subsubsection{حذف الرجوع إلى السطر \texttt{\textbackslash n}}
الدالة
\InlineCode{read}
تستدعي
\InlineCode{fgets}،
إذا تمّ كلّ شيء على ما يرام، ستبحث عن المحرف
\InlineCode{\textbackslash n}
بمساعدة الدالة
\InlineCode{strchr}
الّتي يفترض بك معرفتها. إذا تمّ العثور على
\InlineCode{\textbackslash n}،
فستستبدله بـ\InlineCode{\textbackslash 0}
(نهاية السلسلة) لتجنّب الاحتفاظ بـ"علامة الإدخال".
هاهي الشفرة علّقت عليها خطوة بخطوة:
\begin{Csource}
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // Think to include string.h for strchr()
int read(char *string, int length)
{
char *enterPosition = NULL;
// We read the text
if (fgets(string, length, stdin) != NULL) // No error ?
{
enterPosition = strchr(string, '\n'); // We search for the "Enter"
if (enterPosition != NULL) // If we find the \n
{
*enterPosition = '\0'; // We replace the character by \0
}
return 1; // We return 1 if there's no error
}
else
{
return 0; // We return 0 if there's error
}
}
\end{Csource}
ستلاحظ أنه بالإمكان استدعاء الدالة
\InlineCode{fgets}
مباشرة داخل
\InlineCode{if}.
هذا اختصار كتابي، كي لا أستعمل مؤشّرا يستقبل القيمة المُرجعة من الدالة ثم أختبر قيمته إن كانت
\InlineCode{NULL}
أم لا.
انطلاقا من
\InlineCode{if}
الأول، أعرف هل
\InlineCode{fgets}
عملت على ما يرام أم حدث مشكل ما (قام المستخدم بإدخال محارف أكبر من العدد المسموح به).
إذا تم كل شيء بشكل جيد، سأذهب للبحث عن الـ\InlineCode{\textbackslash n}
باستعمال الدالة
\InlineCode{strchr}
ثم استبداله بالمحرف
\InlineCode{\textbackslash 0}
كما في الشكل التالي.
\begin{figure}[H]
\centering
\includegraphics[width=0.3\textwidth]{Chapter_II-10_string}
\end{figure}
هذا المخطط يبيّن أن السلسلة الّتي تمت قراءتها من طرف الدالة
\InlineCode{fgets}
هي
"\InlineCode{NEBRA\textbackslash n\textbackslash 0}"،
ثم قمنا باستبدال الـ\InlineCode{\textbackslash n}
بـ\InlineCode{\textbackslash 0}
و هذا ما أعطانا
"\InlineCode{NEBRA\textbackslash 0\textbackslash 0}".\\
ليس مشكلا أن توجد علامتا
\InlineCode{\textbackslash 0}
متتابعتين. الحاسوب سيتوقف عند الإشارة الأولى ويعتبرها نهاية السلسلة.
و النتيجة؟ حسنا، لقد عملت بشكل جيّد.
\begin{Csource}
int main(int argc, char *argv[])
{
char name[10];
printf("What's your name ? ");
read(name, 10);
printf("Ah ! Your name is %s !\n\n", name);
return 0;
}
\end{Csource}
\begin{Console}
What's your name ? NEBRA
Ah ! Your name is NEBRA !
\end{Console}
\subsubsection{تفريغ المتغيّر المؤقّت}
لم نصل بعد إلى نهاية الأمور المزعجة.\\
نحن لم نقم بتحليل الحالة التي يقوم فيها المستعمل بإدخال محارف أكثر من ما هو مسموح به!
\begin{Console}
What's your name ? Jean Edouard Albert 1er
Ah ! Your name is Jean Edou !
\end{Console}
بما أن الدالة
\InlineCode{fgets}
تدعم الحماية، فهي توقفت عند الحرف التاسع الذي قام المستعمل بإدخاله لأنّنا حجزنا جدولا من 10 محارف (يجب عدم نسيان أنّ العاشر محجوز لإشارة نهاية السلسلة).
المشكل هو أن بقيّة السلسلة الّتي لم تتم قراءتها.
"\textenglish{ard Albert 1er}"
لم تختف! وإنّما لازالت موجودة في المتغير المؤقّت. هذا المتغير المؤقت هو مكان في الذاكرة يعمل كوسيط بين لوحة المفاتيح والجدول الّذي سيتم تخزين السلسلة فيه. في الـ\textenglish{C}،
لدينا مؤشّر نحو المتغير المؤقت، وهو
\InlineCode{stdin}
الذي تكلمنا عنه قبل قليل.
أعتقد أن مخططا صغيرا سيساعد على توضيح الأمور.
\begin{figure}[H]
\centering
\includegraphics[width=0.6\textwidth]{Chapter_II-10_buffer}
\end{figure}
حينما يقوم المستعمل بإدخال نص بلوحة المفاتيح، فإن نظام التشغيل
(\textenglish{Windows}
مثلا) يقوم بنسخ النص مباشرة في المتغير المؤقّت
\InlineCode{stdin}.
مهمة الدالة
\InlineCode{fgets}
هي إحضار المحارف الموجودة في المتغير المؤقت ووضعها في الذاكرة التي قمت أنت بتحديدها (الجدول
\InlineCode{string}).\\
بعد القيام بعملها، تمسح ما قامت بنسخه من المتغير المؤقّت.
إذا عمل كل شيء على ما يرام، فالدالة
\InlineCode{fgets}
ستقوم بإفراغ كل محتوى المتغير المؤقّت، أي أن هذا الأخير سيكون فارغًا بعد استدعاء الدالة. لكن في الحالة التي يقوم المستخدم بإدخال كثير من المحارف لا يمكن أن يسعها المكان المحجوز لها، فإنه يتم مسح الحروف التي تمت قراءتها فقط، وبالتالي بعد استدعاء الدالة
\InlineCode{fgets}
فإن المتغير المؤقّت يحتوي دائما المحارف المتبقّية!
فلنجرّب مع سلسلة كبيرة:
\begin{Csource}
int main(int argc, char *argv[])
{
char name[10];
printf("What's your name ? ");
read(name, 10);
printf("Ah ! Your name is %s !\n\n", name);
return 0;
}
\end{Csource}
\begin{Console}
What's your name ? Jean Edouard Albert 1er
Ah ! Your name is Jean Edou !
\end{Console}
الدالة
\InlineCode{fgets}
قامت بنسخ المحارف التسعة الأولى كما كان متوقّعا. المشكل هو أن المحارف المتبقية لازالت في المتغير المؤقت!
\begin{figure}[H]
\centering
\includegraphics[width=0.6\textwidth]{Chapter_II-10_buffer_rest}
\end{figure}
هذا يعني أنه لو استدعينا الدالة
\InlineCode{fgets}
مرة أخرى فإنها ستقوم بقراءة ما كان متبقّيا في المتغير المؤقت!
فلنجرّب هذه الشفرة:
\begin{Csource}
int main(int argc, char *argv[])
{
char name[10];
printf("What's your name ? ");
read(name, 10);
printf("Ah ! Your name is %s !\n\n", name);
read(name, 10);
printf("Ah ! Your name is %s !\n\n", name);
return 0;
}
\end{Csource}
نحن نقوم باستدعاء الدالة
\InlineCode{read}
مرّتين لكنك ستلاحظ أنّه لن يتم السماح لك بإدخال اسمك مرتين، وذلك لأن الدالة
\InlineCode{fgets}
لن تطلب من المستخدم إدخال أيّ نصّ في المرّة الثانية لأنّها ستجده في المتغير المؤقت!
\begin{Console}
What's your name ? Jean Edouard Albert 1er
Ah ! Your name is Jean Edou !
Ah ! Your name is ard Alber !
\end{Console}
إذا قام المستعمل بإدخال محارف كثيرة، فإن الدالة
\InlineCode{fgets}
ستحمي البرنامج من مشكل تجاوز الذاكرة، لكن يبقى دائما آثار النصّ في المتغير المؤقت. لذا يجب تفريغ هذا الأخير.
سنقوم إذا بتحسين عمل الدالة
\InlineCode{read}،
و سنقوم في الحالات التي تتطلب ذلك باستدعاء دالة نسميها
\InlineCode{clearBuffer}،
لكي نتأكد من تفريغ المتغير المؤقت في حال ما احتوى على محارف زائدة:
\begin{Csource}
void clearBuffer()
{
int c = 0;
while (c != '\n' && c != EOF)
{
c = getchar();
}
}
int read(char *string, int length)
{
char *enterPosition = NULL;
if (fgets(string, length, stdin) != NULL)
{
enterPosition = strchr(string, '\n');
if (enterPosition != NULL)
{
*enterPosition = '\0';
}
else
{
clearBuffer();
}
return 1;
}
else
{
clearBuffer();
return 0;
}
}
\end{Csource}
الدالة
\InlineCode{read}
تستدعي الدالة
\InlineCode{clearBuffer}
في حالتين:
\begin{itemize}
\item السلسلة المدخلة طويلة جدًا (يمكننا أن نعرف ذلك بعدم وجود الإشارة
\InlineCode{\textbackslash 0}
في السلسلة المنسوخة).
\item إذا حدث أي خطأ مهما كان، يجب تفريغ محتوى المتغير المؤقت لأسباب حماية لكي لا يبقى شيء هناك.
\end{itemize}
الدالة
\InlineCode{clearBuffer}
قصيرة لكنها عميقة. فهي تقرأ المتغير المؤقت محرفًا محرفًا باستعمال الدالة
\InlineCode{getchar}.
هذه الدالة تقوم بإرجاع
\InlineCode{int}
(وليس
\InlineCode{char}،
سوف تعرف السبب لاحقا، أيّا يكن).\\
سنكتفي نحن باسترجاع القيمة في متغير
\InlineCode{c}
من نوع
\InlineCode{int}.
نقوم بحلقة تكرارية مادمنا لم نقرأ بعد المحرف
\InlineCode{\textbackslash 0}
أو الرمز
\InlineCode{EOF}
(نهاية الملف) وهما يعنيان: لقد وصلت إلى نهاية المتغير المؤقت. سنتوقّف عند الوصول إلى أحد هذين المحرفين.
يبدو عمل الدالة
\InlineCode{clearBuffer}
صعبًا قليلًا لكنها تقوم بعملها. لا تتردد في تكرار قراءة الشرح عدة مرات من أجل الفهم الجيد.
\section{تحويل سلسلة محرفيّة إلى عدد}
دالتنا
\InlineCode{read}
هي فعالة وقويّة الآن، لكنها تجيد قراءة النصوص فقط. ستتساءل حتما: "لكن كيف نقوم باسترجاع عدد؟"
الحقيقة أن الدالة
\InlineCode{fgets}
هي دالة مبدئية. مع
\InlineCode{fgets}
لا يمكن قراءة سوى النصوص، ولكن توجد دوال أخرى تقوم بتحويل النص إلى عدد.
\subsection{\texttt{strtol}: تحويل سلسلة محرفيّة إلى \texttt{long}}
نموذج هذه الدالة خاص نوعًا ما:
\begin{Csource}
long strtol(const char *start, char **end, int base);
\end{Csource}
الدالة ستقوم بقراءة السلسلة المحرفيّة المرسلة إليها
(\InlineCode{start})
و ستحاول تحويلها إلى
\InlineCode{long}
باستعمال الأساس
(\InlineCode{base})
المحدد (غالبًا ما نستعمل الأساس 10، لأننا نستعمل الأرقام من 0 إلى 9، ولهذا ضع مكانه العدد 10). ستقوم بإرجاع العدد الذي نجحت في قراءته.\\
بالنسبة لمؤشر المؤشر
\InlineCode{end}،
فالدالة ستقوم باستغلاله لإرجاع أول محرف صادفته ولم يكن رقمًا. لكننا لسنا بحاجة إليه، فلنكتفي بوضع
\InlineCode{NULL}
مكانه لنقول أننا لا نريد استرجاعه.
على السلسلة المحرفيّة أن تبدأ برقم، فبعد الأرقام كل شيء يتم تجاهله. يمكن أن تكون مسبوقة بفراغات.\\
هذه أمثلة للفهم الجيد:
\begin{Csource}
long i;
i = strtol("148", NULL, 10); // i = 148
i = strtol("148.215", NULL, 10); // i = 148
i = strtol(" 148.215", NULL, 10); // i = 148
i = strtol(" 148+34", NULL, 10); // i = 148
i = strtol(" 148 dead leaves", NULL, 10); // i = 148
i = strtol( " There are 148 dead leaves", NULL, 10 ); // i = 0 (error : The string doesn't start with a number)
\end{Csource}
كل السلاسل التي تبدأ برقم (أو ربما بفراغات قبله) سيتم تحويلها إلى
\InlineCode{long}
حتى الوصول إلى محرف غير مقبول (نقطة، فاصلة، علامة استفهام، زائد، إلخ).
بالنسبة لسلسلة لا تبدأ بأرقام وأو بفراغات تليها أرقام، فلا يمكن تحويلها وبالتالي تقوم الدالة بإرجاع القيمة~0.
يمكننا كتابة الدالة
\InlineCode{readLong}،
و التي تقوم باستدعاء الدالة
\InlineCode{read}
(لقراءة النص)، وبعد ذلك تحويل النص إلى عدد:
\begin{Csource}
long readLong()
{
char textNumber[100] = {0}; // 100 cells are sufficient
if (read(textNumber, 100))
{
// If we read the text without problems, we convert textNumber to long and we return it
return strtol(textNumber, NULL, 10);
}
else
{
// If there's a problem, we return 0
return 0;
}
}
\end{Csource}
يمكنك تجريب الشفرة داخل
\InlineCode{main}
بسيط.
\begin{Csource}
int main(int argc, char *argv[])
{
long age = 0;
printf("How old are you ? ");
age = readLong();
printf("Ah ! You are %d years old !\n\n", age);
return 0;
}
\end{Csource}
\begin{Console}
How old are you ? 18
Ah ! You are 18 years old!
\end{Console}
\subsection{\texttt{strtod} تحويل سلسلة محرفيّة إلى \texttt{double}}
الدالة
\InlineCode{strtod}
مطابقة للدالة
\InlineCode{strtol}،
الفرق الوحيد هو أنها ستحاول قراءة عدد عشريّ وإرجاع
\InlineCode{double}.
\begin{Csource}
double strtod(const char *start, char **end);
\end{Csource}
تجد أن المعامل الثالث
\InlineCode{base}
اختفى هنا، بينما يبقى مؤشّر المؤشّر
\InlineCode{end}
الّذي لا يفيدنا في شيء.
على خلاف الدالة السابقة، فإن هذه الدالة ستأخذ في الحسبان "النقطة" العشرية. عندما أقول نقطة يعني أن الدالة لا تقبل الفاصلة
"\textenglish{,}"
(يبدو أنّها مبرمجة من طرف ناطقين بالإنجليزية).
فلتقم بكتابة الدالة
\InlineCode{readDouble}
بنفسك. إن كتابتها مماثلة للدالة
\InlineCode{readLong}،
الاختلاف الوحيد هو أنها ستستدعي الدالة
\InlineCode{strtod}
ثم ستقوم بإرجاع قيمة
\InlineCode{double}.
يعني أنك ستكتب التالي في الكونسول:
\begin{Console}
What's your weight ? 67.4
Ah ! Your weight is 67.400000 kg !
\end{Console}
حاول بعد ذلك تعديل الدالة
\InlineCode{readDouble}
لتقبل الفاصلة أيضًا كفاصل عشري. إن الأمر بسيط: فقط قم باستبدال كل تكرار للمحرف
"\InlineCode{,}"
بالمحرف
"\InlineCode{.}"
(بالاستعانة بالدالة
\InlineCode{strchr})،
ثم قم ببعث النص الجديد إلى الدالة
\InlineCode{strtod}.
\section*{ملخّص}
\begin{itemize}
\item الدالة
\InlineCode{scanf}
بالرغم من أنها تبدو سهلة وطبيعية إلا أنها معقّدة وتفرض علينا بعض الحدود. فمثلًا، هي لا تقبل قراءة نص يحتوي فراغات.
\item نقول أننا تسببنا في الـ\textenglish{buffer overflow}
إذا تجاوزنا المساحة المخصصة في الذاكرة، فمثلًا لو قام المستخدم بإدخال 10 محارف ونحن قد حجزنا 5 خانات فقط.
\item الحل الأمثل هو استدعاء الدالة
\InlineCode{fgets}
لتقوم باسترجاع النص الذي يُدخله المستعمل.
\item يجب أن تتجنب استعمال الدالة
\InlineCode{gets}
بأي ثمن لأنّها لا تحمي من الـ\textenglish{buffer overflow}.
\item يمكنك كتابة دالة خاصة بك، تقوم باستدعاء الدالة
\InlineCode{fgets}
كما فعلنا لكيّ تحسّن عملها.
\end{itemize}