چیه ماجرا؟
امروز ۱۵ اسفنده و روز هفتم جنگ. طبعاً مثل هر آدم دیگهای من هم تجربهای مرکب از حسهای مختلف رو دارم تجربه میکنم و برای سرگرم کردن خودم، یه پروژه تعریف کردم. تو این پست میبینم که آرم چقد قشنگه و مثل اون اسمبلی X86 رجیسترهای عجق وجق و اینستراکشنای قوری ووری نداره. و قراره با اسمبلی آرم یه برنامه بنویسیم که یه آیپی تو فرمت CIDR (123.12.23.13/31) بگیره و بهمون بگه نتمسکش، نتورکآدرس، برودکستآدرس و اولین و آخرین آدرسهای قابل استفادهی رنج چیان.
ماجرای اسمبلی آرم رو هم با فایلهایی که اینجا و اینجا و اینجا هست پی میگیرم.
نتمسک و ماجراهاش
کل ماجرا رو میشه تو این کد جاوااسکریپت داشت:
1class IP {
2 constructor(ip, mask) {
3 this.ip = ip;
4 this.mask = mask;
5
6 this.parts = ip.split(".").map(e => parseInt(e.trim()));
7 }
8
9 netmaskAddr() {
10 const octets = [0, 0, 0, 0];
11
12 for (let i = 0; i < this.mask; i++) {
13 octets[Math.floor(i / 8)] += Math.pow(2, 7 - (i % 8));
14 }
15
16 return new IP(octets.join("."));
17 }
18
19 subnetAddr() {
20 const octets = [0, 0, 0, 0];
21
22 for (let i = 0; i < this.mask; i++) {
23 octets[Math.floor(i / 8)] += Math.pow(2, 7 - (i % 8));
24 }
25
26 for (let i = 0; i < 4; i++) {
27 octets[i] &= this.parts[i];
28 }
29
30 return new IP(octets.join("."));
31 }
32
33 broadcastAddr() {
34 const octets = [0, 0, 0, 0];
35
36 for (let i = this.mask; i < 32; i++) {
37 octets[Math.floor(i / 8)] += Math.pow(2, 7 - (i % 8));
38 }
39
40 for (let i = 0; i < 4; i++) {
41 octets[i] |= this.parts[i];
42 }
43
44 return new IP(octets.join("."));
45 }
46
47 firstAddress() {
48 const subnet = this.subnetAddr();
49 subnet.parts[3]++;
50
51 return new IP(subnet.parts.join("."));
52 }
53
54 lastAddress() {
55 const broadcast = this.broadcastAddr();
56 broadcast.parts[3]--;
57
58 return new IP(broadcast.parts.join("."));
59 }
60
61 toString() {
62 let s = this.ip;
63 if (this.mask) {
64 s += "/" + this.mask;
65 }
66
67 return s;
68 }
69}
کل ماجرای نتمسک چیزی نداره. نتمسک میاد ۳۲بیت آیپی رو به دو بخش تقسیم میکنه: نتورک و هاست. اون عدد که بعد از اسلش میاد میگه چند بیت مال نتورکه و باقی چیزی که میمونه میشه بیتهای هاست. یچی مثل کد اموال تو شرکتها مثلاً: ما-چپ-۱۱ میشه گروه مالی، سمت چپ ساختمون، مال شمارهی ۱۱. گروه مالی و سمت چپ میشه قسمت نتورک و ۱۱ هم میگه اونجا کی کجاست. پس:
- خود نتمسک میشه
0xFFFFFFFFشیفت چپ به اندازهی ۳۲ منهای مسک. - نتورکآدرس میشه & منطقی نتمسک با آیپی
- برودکست میشه نقیض نتمسک و یای | لاجیکال با آیپی
- در نهایت هم اولین آدرس قابل استفاده میشه آخرین اکتت نتورکآدرس + ۱
- و آخرین آدرس قابل استفاده هم میشه اکتت آخر برودکستآدرس منهای یک.
قراره چطور پیش بریم؟
ماجرا احتمالاً خیلی سخت نباشه در عمل ولی برای چیزی که هنوز هیچ دیدی نداریم چه اتفاقی قراره تو مسیر برامون بیفته بهتر بشکنیمش. چیزی که الان دارم بهش فکر میکنم اینه:
- یه تیکه کد که یچی چاپ کنه
- یه تیکه کد که بتونه یه عدد بخونه از ورودی و اون رو چاپ کنه
- یه تیکه کد که بتونه روی یه اکتت جمع بزنه
- یه آیپی CIDR رو بخونه و تبدیلش کنه به یه صحیح ۳۲بیتی و بایت ماسک
- نتمسک رو بسازه
- چهارتا آیپی رو با استفاده از ماسک بسازه
- چاپ کنیم حاصل رو
پیادهسازی
یه تیکه کد که یچی چاپ کنه
1.global _start
2
3.section .data
4 msg: .ascii "Hello, World\n"
5 msg_len= . - msg
6
7.section .text
8_start:
9 mov w8, #64 ; w8 <- 64
10 mov w0, #1 ; w0 <- 1
11 ldr w1, =msg ; w1 <-(from memory)- *(msg)
12 ldr w2, =msg_len ; w2 <- msg_len = 13
13 svc #0
14
15 mov w8, #93
16 mov w0, #0
17 svc #0
احتمالاً اگه از قبل هیچ برخوردی با اسمبلی نداشته باشیم، قسمت استارت ماجرا یذره گنگ میزنه.
سیسکالها
تو این کد ما سیسکال write رو صدا زدیم، بهش گفتیم داخل خروجی استاندارد مسیجی که بهت میگیم رو که طولش هم فلانقدره بنویس. بعدش هم سیسکال exit رو صدا زدیم و گفتیم با کد صفر خارج شو. اینجاست که برای من زیبایی اسمبلی آرم نسبت به دوستاش خودش رو نشون میده. رجیسترها مثل آدمیزاد بنابر سایز یه حرف گرفتن (در اینجا w که میشه اول word که همون ۳۲بیته تو ادبیات مرتبط و اون یکی که x هست که از extended word گرفته شده و میشه ۶۴ بیته) و از صفر تا ۳۱ شمارهگذاری کردن. راحت! خبری از eax و اینا نیست (من یدوره حداقل یک ماهه تلاش کردم اونا رو حفظ کنم و الان هم یادم نمیاد کی کجا بود و چی بود).
برای اینکه چیزی که در بالا گفتیم رو ترجمه کنیم، کافیه بدونیم که همیشه تو ورد ۸ شمارهی سیسکال میاد که این داکیومنت (از این سایت) زیبا و قشنگ ماجرا رو روشن میکنه (ستون اول شمارهی سیسکالهاست، یا ستون w8 که مقدار هگز همونه) و وردهای صفر تا پنج هم آرگومانهای صفر تا پنج سیسکال رو مشخص میکنن. برای مثال امضای تابع write اینه:
1write(unsigned int fd, const char *buf, size_t count)
پس ما تو ورد هشت مقدار ۶۴ رو گذاشتیم که میشه سیسکال رایت، تو ورد صفر مقدار ۱ رو گذاشتیم یعنی فد خروجی استاندارد، تو ورد یک آدرس پیام رو لود کردیم از مموری و تو ورد دو هم طول رشته رو گذاشتیم. با svc (که مخفف Supervisor Callعه) هم که سیسکال رو کال کردیم. همین اتفاق برای سیسکال exit هم افتاده.
سکشن دیتا
یه توضیح کوچیک هم در مورد بخش data اینکه طبعاً سکشن دیتا برای تعریف کردن دادهها (متغیرها) استفاده میشه. در کد بالا ما اومدیم یه تیکه از مموری رو گرفتیم و رشتهمون رو گذاشتیم داخلش و برای اینکه بعداً بتونیم بهش دسترسی داشته باشیم، باید آدرسش رو برچسب بزنیم. msg یه برچسبه برای مکانی از مموری (بر چسبها همیشه با : تعریف میشن) و بعد از جنریت شدن اون رشته یچی میشه مثل ۱۲۰۰ (یه آدرس از مموری). تو خط بعد = همون عملگر اساینمنته که تو زبانهای دیگه هست (فقط در اینطور موقعیتی) و میاد میگه مقدار msg - . رو به این ثابت (با اغماض) اساین کن (چون ثابته دیگه آدرسی در مموری هم نداره که بخواد با : برچسب بزنیم بهش). نقطه هم موقعیت مموریه در اونجایی که هست. یعنی بعد از رشتهی ۱۳ حرفیای که داریم، نقطه به آدرس ۱۲۱۳ اشاره میکنه. حالا تفریق این دوتا آدرس میشه طول رشته و اون مقدار به msg_len اساین میشه.
و حالا که تا اینجا اومدیم، بعد از اسمگذاری رجیسترها که خیلی راحتتر از اسمبلی X86عه، به تفاوت دوم این دوتا معماری هم رسیدیم. داخل معماری آرم، فقط میشه روی چیزهایی محاسبات و چک و تیک داشت که داخل رجیسترها باشن و از سر همین با Load and Store خیلی داخل این معماری روبرو میشیم (LDR در واقع مخفف LoaD Register هست و در کنارش STR که میشه STore Register).
ذخیره/بازیابی و ماجرای آدرسدهی
برای فهمیدن LDR و STR باید اول دقت کنیم که مموری رو میشه به شکل یه آرایه از بایتها دید که اندیس اون آرایهها میشه آدرس مموری. تو آرم۶۴ این آدرسها ۶۴ بیتن. طبعاً وقتی یه بخشی از حافظه رو رزرو میکنیم آدرسهای اون بخش هم پشت سر هم میان و میشه از جمع و تفریق روی آدرسها استفاده کرد. مثل کاری که برای گرفتن طول رشته در کد بالا کردیم (رشته در واقع آرایهای از بایتهاست). یا مثلاً میشه اگه آدرس یه المان آرایه رو داشت المان بعدیش رو گرفت و از اینجور کارها (داخل سی کار با آرایهها عملاً همین بود).
شکل کلی استفاده از LDR و STR اینجوریه:
1ldr Rn, addressing_mode
2str Rn, addressing_mode
که در اون R یا Xعه که باعث میشه روی ۶۴ بیت از حافظه ذخیره انجام بشه یا خونده بشه و یا Wعه که باعث میشه این اتفاق روی ۳۲ بیت از حافظه باشه (در واقع W و X هر دو یه رجیسترن و ما ۳۲ یا ۶۴ بیت از اونها رو استفاده میکنیم). addressing mode هم شیوهای هست که دستورمون استفاده میکنه تا آدرسی از مموری که نیاز هست رو پیدا کنه. برای محاسبهی آدرس میشه از شیوههای زیر استفاده کرد (شیوهها برای هر دو دستور کار میکنن ولی برای سادگی و عدم تکرار فقط ldr رو میاریم):
پایه: در این شیوه آدرس در یه رجیستر ذخیره شده و ما از اون آدرس استفاده میکنیم و محتوا رو ذخیره یا لود میکنیم.
1ldr x1, [x0] // x1 <- *(x0)پایه جمع با آفست: اگه آدرس در رجیستر ذخیره شده باشه و ما بخوایم چندتا آفست اینور اونورش رو بخونیم. آفست رو در محاسبهی طول رشته در بالا گفتیم که چیه.
1ldr x1, [x0, #1] // x1 <- *(x0 + 1) 2ldr x1, [x0, #-16] // x1 <- *(x0 - 16)در این شیوه یه سری حالتها (مثل پردازش آرایه یا پردازش روی استک که جلوتر میبینیم) هست که نیازه محتوای یه آدرس رو در آفستهای منظم بخونیم (مثلاً برای یه آرایه یدونه یدونه بریم جلو). راه سادهست، هر بار که لود میکنیم رجیستر آفست رو یکی افزایش میدیم و دوباره میخونیم و افزایش میدیم تاااا کار تموم شه. برای اینکه هر بار با یه دستور جدا آفست رو جلو عقب نکنیم، آرم دوتا میانبر در اختیار گذاشته:
- آدرسدهی پیشا-اندیس (pre-index addressing): در این حالت اول اندیس رو افزایش/کاهش میده و بعد حافظه در آدرس جدید رو لود/ذخیره میکنه. شیوهی استفادهاش اینطوریه:این حالت بیشتر وقتی به کار میاد که میخوایم یه چیزی رو تو آرایه یا استک ذخیره کنیم و اگه روی همون اندیس فعلی ذخیره کنیم، مقدار فعلی از بین میره، پس قبل از ذخیره اندیس رو افزایش میدیم.
1ldr x1, [x0, #1]! // x0 = x0+1 and then x1 <- *(x0) - آدرسدهی پسا-اندیس (post-index addressing): در این حالت اول مقدار حافظه رو در آدرس فعلی لود/ذخیره میکنه و بعد از خوندن/ذخیره، آدرس رو افزایش/کاهش میده:یه مثال از کاربرد این حالت هم وقتاییه که میخوایم اول مقدار آرایه یا استک در اندیس فعلی رو بخونیم/ذخیره کنیم و بعد اندیس رو کاهش/افزایش بدیم.
1ldr x1, [x0], #1 // x1 <- *(x0) and then x0=x0+1
- آدرسدهی پیشا-اندیس (pre-index addressing): در این حالت اول اندیس رو افزایش/کاهش میده و بعد حافظه در آدرس جدید رو لود/ذخیره میکنه. شیوهی استفادهاش اینطوریه:
پایه جمع با رجیستر آفست: اگه آدرس در رجیستر ذخیره شده باشه و آفستی که میخوایم بخونیم هم در یه رجیستر ذخیره شده.
1ldr x2, [x0, x1] // x2 <- *(x0 + x1)و در نهایت خوندن یه متغیر:
1.section .data 2my_var: .space 16 3 4.section .text 5_start: 6ldr x0, my_var // x0 <- &my_var (address of my_var) 7ldr x1, [x0] // x1 <- *(x0) (value stored in my_var)همونطور که مشخصه اینطور میتونیم مموری رو وصل کنیم به رجیسترها و عملاً از اینجا میشه برنامهی واقعی نوشت. ما بالاتر از همین شیوه استفاده کردیم برای خوندن رشتهای که ساختیم ولی اونجا شیوهی لودمون با
=بود. در واقع=اینجا یه شبهعملگره و میاد کار رو میسپره به خود کامپایلر و اکسپنشن رو خود کامپایلر انجام میده (تو زمینه همین کارایی که ما کردیم رو بسته به اینکه اون آدرس چی توش باشه و چقد باشه و اینا انجام میده).علت این اتفاق هم که مشخصه. هر دستور آرم۶۴ دقیقاً ۳۲ بیته و اگه فایل ISA که در بالا بود رو ببینید، قالب ۳۲ بیتی هر دستور و کاربرد هر بیت داخلش مشخصه. از اونور ما گفتیم که آدرسها داخل آرم۶۴ دقیقاً ۶۴ بیتن و خیلی واضحه که نمیشه هیچ جوره مستقیم یه مقدار ۶۴ بیتی رو توی ۳۲ بیت جا داد.
خیلی وقتها هم نیازه که دوتا رجیستر رو لود/ذخیره کرد. برای این کار هم آرم دستورات
ldpوstpرو معرفی کرده که addressing modeش مثل بالاست ولی دوبار کار ذخیره/بازیابی رو با آفست ۸ انجام میده:1ldp x0, x1, [x2] // x0 <- *(x2) and x2 <- *(x2+8) 2stp x0, x1, [x2, #-16]! // x2' = x2-16, then *(x2') = x0 and *(x2'+8) = x1 3 // *(x2-16) = x0 and *(x2-8) = x1
برای پایان این بخش کافیه کد رو کامپایل و لینکش کنیم و نتیجه رو ببینیم:
1meysam@ubuntu:~/www/test/arm/netmask$ as -o main.o main.s
2meysam@ubuntu:~/www/test/arm/netmask$ ld -o main main.o
3meysam@ubuntu:~/www/test/arm/netmask$ ./main
4Hello, World
5meysam@ubuntu:~/www/test/arm/netmask$ echo $?
60
و چون تو پست داکر از صفر: پروسه هم strace رو دیده بودیم، باحاله که خروجی رو برای فایلمون ببینیم:
1meysam@ubuntu:~/www/test/arm/netmask$ strace -f ./main
2execve("./main", ["./main"], 0xffffda37aea8 /* 25 vars */) = 0
3write(1, "Hello, World\n", 13Hello, World
4) = 13
5exit(0) = ?
6+++ exited with 0 +++
:)). کوچولوووو.
یه تیکه کد که بتونه یه عدد بخونه از ورودی و اون رو چاپ کنه
اول یه کد بنویسیم که یه عدد رو چاپ کنه.
1.global _start
2
3.section .text
4_start:
5 mov w8, #64
6 mov w0, #1
7 mov w1, #5
8 mov w2, #1
9 svc #0
10
11 mov w8, #93
12 mov w0, #0
13 svc #0
داخل رجیستر و۱ مقدار ۵ رو گذاشتیم و طولش رو ۱ ست کردیم و انتظار داریم که چاپش کنه:
1meysam@ubuntu:~/www/test/arm/netmask$ as -o main.o main.s
2meysam@ubuntu:~/www/test/arm/netmask$ ld -o main main.o
3meysam@ubuntu:~/www/test/arm/netmask$ ./main
4meysam@ubuntu:~/www/test/arm/netmask$
ولی نکرد. و خب عجیب هم نیست. اگه به پارامتر دوم write برگردیم const char *buf از ما انتظار تایپ پوینتر کاراکتر رو داره. ولی چیزی که ما بهش دادیم مثل یه آدرس تو ناکجا آباده:
1execve("./main", ["./main"], 0xffffcdab8bf0 /* 25 vars */) = 0
2write(1, 0x5, 1) = -1 EFAULT (Bad address)
3exit(0) = ?
4+++ exited with 0 +++
پس اول نیازه مقدار ۵ رو داخل یه خونهی حافظه ذخیره کنیم و پوینتر اون رو به سیسکالمون پاس بدیم:
1.global _start
2
3.section .data
4 buf: .space 1
5
6.section .text
7_start:
8 mov w1, #5
9 ldr x0, =buf
10 str w1, [x0]
11
12 mov w8, #64
13 mov w0, #1
14 ldr w1, =buf
15 mov w2, #1
16 svc #0
17
18 mov w8, #93
19 mov w0, #0
20 svc #0
و رانش:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffcc078380 /* 25 vars */) = 0
3write(1, "\5", 1) = 1
4exit(0) = ?
5+++ exited with 0 +++
به جای ۵، 5\ رفته برای چاپ. باز اگه به سیسکال رایت برگردیم و پارامتر دوم، به تایپ char میرسیم. تایپ کاراکتر در واقع همون تایپ بایته که با استاندارد اسکی تفسیر میشه. پس عدد ۵ اون چیزی نیست که معادل ۵ اسکی باشه. اگه از man 7 ascii استفاده کنیم، میبینیم که کد اسکی ۵ در واقع ۵۳ هست:
1 30 40 50 60 70 80 90 100 110 120
2 ---------------------------------
30: ( 2 < F P Z d n x
41: ) 3 = G Q [ e o y
52: * 4 > H R \ f p z
63: ! + 5 ? I S ] g q {
74: " , 6 @ J T ^ h r |
85: # - 7 A K U _ i s }
96: $ . 8 B L V ` j t ~
107: % / 9 C M W a k u DEL
118: & 0 : D N X b l v
129: ' 1 ; E O Y c m w
پس با تغییر مقدار ۵ به ۵۳ به چیزی که میخوایم میرسیم:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffdb1e6ba0 /* 25 vars */) = 0
3write(1, "5", 15) = 1
4exit(0) = ?
5+++ exited with 0 +++
6meysam@ubuntu:~/www/test/arm/netmask$ ./main
75
تقریباً کار این زیربخش دیگه تمومه. کافیه سیسکال read رو پیدا کنیم و ببینیم چی میخواد، مقدار رو با اون بخونیم و بریزیم داخل بافری که ساختیم و همون بافر رو هم چاپ کنیم. شمارهی سیسکال ۶۳ هست و امضای سیسکال هم اینطوریه:
1read(unsigned int fd, char *buf, size_t count)
تنها چیزی که فرق میکنه نسبت به راهی که تا اینجا اومدیم اینه که فد رو باید بذاریم صفر که میشه ورودی استاندارد.
1.global _start
2
3.section .data
4 buf: .space 1
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =buf
11 mov w2, #1
12 svc #0
13
14 mov w8, #64
15 mov w0, #1
16 ldr w1, =buf
17 mov w2, #1
18 svc #0
19
20 mov w8, #93
21 mov w0, #0
22 svc #0
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffc44ae9c0 /* 25 vars */) = 0
3read(0, 4
4"4", 1) = 1
5write(1, "4", 14) = 1
6exit(0) = ?
7+++ exited with 0 +++
یه تیکه کد که بتونه روی یه اکتت جمع بزنه
برای شروع از کد مرحلهی قبل استفاده میکنیم و یک اکتت میگیریم و چاپش میکنیم. برای این منظور کافیه ۳ بایت رزرو کنیم (هر کاراکتر ۱ بایت)، ۳تا کاراکتر از ورودی بخونیم و همون رو چاپش کنیم:
1.global _start
2
3.section .data
4 octet: .space 3
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =octet
11 mov w2, #3
12 svc #0
13
14 mov w8, #64
15 mov w0, #1
16 ldr w1, =octet
17 mov w2, #3
18 svc #0
19
20 mov w8, #93
21 mov w0, #0
22 svc #0
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ as -o main.o main.s && ld -o main main.o && ./main
2123
3123
از اسکی به بایت
در ادامه برای اینکه بتونیم جمع بزنیم به همون مشکلی میخوریم که موقع چاپ ۵ داشتیم. بافر از تایپ کاراکتره و اسکی ولی ما عدد میخوایم. اگه فرض کنیم که قرارمون اینه که همیشه عدد بخونیم، با توجه به جدول اسکی که بالاتر بود متوجه میشیم که فاصلهی هر کاراکتر عددی از کاراکتر صفر، همون عدده، پس کافیه از هر کاراکتر مقدار اسکی صفر رو کم کنیم.
پس در ادامه اول روی رشته تلاش میکنیم هر کاراکتر رو بگیریم:
1.global _start
2
3.section .data
4 octet: .space 3
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =octet
11 mov w2, #3
12 svc #0
13
14 ldr x3, =octet
15 mov w4, #3
16 bl str_to_num ; branch with link (the next line address will be saved on LR or x30 register and after calling,
17 ; the main branch continue to execute from there.
18
19 mov w8, #93
20 mov w0, #0
21 svc #0
22
23str_to_num:
24 ldrb w5, [x3] ; w5 <- loadTheFirstByte(x3)
25
26 mov w8, #64
27 mov w0, #1
28 mov w1, w5
29 mov w2, #1
30 svc #0
31
32 add x3, x3, #1 ; x3 <- x3 + 1 (x3 stores the address of a memory location, so by incrementing it, we go to the
33 ; next char)
34
35 subs w4, w4, #1 ; w4 <- w4 - 1 and set flags
36
37 bne str_to_num ; branch to the str_to_num label again if the sat flags are not zero
38
39 ret
قبل از اجرای برنامه، چندتا چیز جدید داخل کد داریم. اول اینکه اینجا اولین جاییه که داخل اسمبلی ما تابع (str_to_num) تعریف کردیم. تابع، داخل اسمبلی چیزی نیست جز یه برچسب که حاوی آدرسیه که شروع تابع اونجاست. برچسب رو داخل سی و پاسکال و گو هم دیدیم. چیزیه که با goto میشه مسیر اجرای برنامه رو به سمتش برد. اینجا به جای goto از b (مخفف branch) استفاده میکنیم.
جریانِ برنامه
برنچ گرفتن رو کلاً میشه به دو دسته تقسیم کرد: برنچ گرفتن غیرشرطی و برنچ گرفتن شرطی. برنچ غیرشرطی از اسمش مشخصه که فقط میگیم برو به اون برچسب. برای این منظور از b و br و bl استفاده میکنیم. b که فقط جریان برنامه رو میبره به یه برچسب مشخص، br میاد برای رفتن به جای مشخص از رجیستر آدرس رو میخونه، و bl (مخفف branch with link) میاد وقتی تابع میخواد کال شه، آدرس دستور بعدی رو در رجیستر LR ذخیره میکنه. اینطور وقتی که ما در آخر ret رو صدا زدیم، کد میاد به جایی که صدا زده شده بوده و ادامهی مسیر رو پی میگیره (در واقع ret همون br LR هست).
برنچهای شرطی که در واقع مثالش در کد بالا، bne هست میاد بر اساس شرطی که چک کردیم تصمیم میگیره بره به یه مسیر یا نره. برای فهمیدن این شکل برنچ گرفتن، خط قبل برنچ گرفتن در کد بالا رو اگه ببینیم تابع subs کال شده. اون s در واقع state هست و برای فهمیدنش یچی دیگه رو باید بفهمیم :)).
حالت پردازشگر
آرم۶۴ یه رجیستر سیستمی به اسم PSTATE (که تو نسخههای قبلی سیستمی نبوده و اسمش PSR مخفف Program Status Register بوده) داره که مخفف Processor State هست و یه سری اطلاعات مشخص رو در خودش نگه میداره. به صورت عادی نمیشه به این رجیستر دسترسی داشت و با دستورهای خاص mrs (برای خوندن) و msr (برای نوشتن) میشه مقادیر چندتا viewطور از این رجیستر رو خوند.
ویوی NZCV
همینطور که تا اینجا دیدیم، داخل اسمبلی خیلی خبری از معجزه و ژینگولبازی و اینا نیست. یکی از این موقعیتها که شاید در زبانهای دیگه خیلی اصن متوجهش هم نباشیم اینه که به صورت مستقیم نمیشه فهمید جمع، تفریق و ترکیب دوتا رجیستر کی صفر شد، کی منفی شد و کی آورفلو داد. دستورهای مرتبط با این اعمال میان کارشون رو میکنن و میرن و برای وقتی که برای ما مهمه که آیا این اتفاقها افتاده یا نه، PSTATE میاد استیت رو برامون نگه میداره. ماجرا هم به این صورته که وقتی یه سری توابع مشخص روی رجیسترها کال میشن، بر اساس مقدار حاصل، بیتهای ۲۸ تا ۳۱ PSTATE بروز میشن و این بیتها هر کدوم میشن یه فلگ که با اون میشه سوالی که داشتیم رو پاسخ داد.
| فلگ | ریشه | شمارهی بیت | توضیح |
|---|---|---|---|
| N | Negative | ۳۱ | حاصل منفی بوده یا نه (مقدار بیت ۳۱ یا ۶۱ بسته به سایز رجیستر) |
| Z | Zero | ۳۰ | حاصل صفر بوده یا نه |
| C | Carry | ۲۹ | سرریز/قرض گرفتن در حالت بدون علامت رخ داده/نداده (صفر) یا نداده/داده (یک) |
| V | oVerflow | ۲۸ | سرریز در حالت علامتدار رخ داده (یک) یا نه (صفر) |
یه نکته که رو باید بهش توجه کنیم اینه که داخل رجیستری ما رجیستر با/بدون علامت (برای اعداد صحیح طبعاً) نداریم. رجیستر ۳۲ یا ۶۴ بایتی داریم و وقت چک کردن، خودمون باید منظور خودمون از اون رجیستر رو بدونیم و بر مبنای اون چکمون رو انجام بدیم. با این وصف، دوتا ردیف اول که نکتهی خاصی نداره واقعیت. میمونه آورفلوها. برای در آوردن ته آورفلوها باید یچی رو قبلش بررسی کنیم.
مکمل ۲
گفتیم که داخل اسمبلی، رجیسترهامون تایپ علامتدار یا بدون علامت ندارن و صرفاً یه مشت صفر و یک رو ذخیره میکنن. تفسیر ما از صفر و یکهاست که میتونه اونا رو علامتدار یا بدون علامت کنه.
تایپ بدون علامت سادهست. مقدار صفر رو ذخیره میکنه تا $2^n - 1$. برای نمایش علامت میتونیم بیت آخر رو بذارم برای علامت و اینطور از $-2^n -1$ تا $2^n -1$ رو میتونیم نشون بدیم ولی دوتا مشکل وجود داره، اول اینکه اینطور دوتا صفر داریم (همه بیتا صفر و همه بیتا صفر به جز بیت آخر) و دوم اینکه یکپارچگی محاسباتی بین سایزهای ۳۲ و ۶۴ از بین میره: برای نمایش اعداد منفی در سایزهای ۳۲ و ۶۴ باید کار اضافه انجام داد. این شد که ملت اومدن از یه مفهوم جبری استفاده کردن: حلقه.
حلقه
حلقه یا ring یه ساختار جبریه که الان کاری به همهی خاصیتهاش نداریم به جز سهتا: بسته بودن نسبت به عملگر جمع (یعنی حاصل جمع دوتا عدد داخل حلقه، دوباره داخل همون حلقه میافته)، وجود عنصر یکتای همانی جمع (که میشه همون صفرمون) و وجود عنصر وارون جمع (که میشه المانی که اگه با المان دیگه جمع شه میشه عنصر همانی). از بین همهی حلقهها هم یکی هست که الان به کار ما میاد: حلقهی اعداد صحیح با پیمانهی $2^n$.
پیمانه همون باقیمانده به زبان ریاضیدانهاست :)). احتمالاً از ریاضی دبیرستان یادتون باشه همنهشتی رو. میگفتیم دوتا عدد به پیمانهی ایکس همنهشتن اگه تفریق دوتا عدد بر ایکس بخشپذیر باشه. و بعدش هم رابطهی افراز تعریف میکردیم و میگفتیم رابطهی همنهشتی میاد کلاسهای همارزی تعریف میکنه. همهی اینا رو گفتم که بگم مجموعهی همهی کلاسهای همارزی با یک پیمانه، تشکیل یک حلقه میده که بهش میگن حلقهی اعداد صحیح با پیمانهی ایکس. پس عنصر همانی جمع یکتاست توش و عنصر وارون جمع هم وجود داره و یکتاست.
حالا اگه فرض کنیم تعداد بیتهامون ۳عه، حلقهی اعداد صحیح با پیمانهی سه ما رابطهاش این شکلی میشه
$$a - b ≡ 2^3$$که این یعنی ما صفر داریم داخل این مجموعه و برای عنصر داخلش هم یک عنصر یکتا داریم که اگه باهاش جمع شه میشه صفر. از رابطهی بالا ما چندتا نتیجه میگیریم، اول اینکه صفر با $2^3$ همارزه. دومین نتیجهای که میتونیم بگیریم اینه که ۷ با منفی ۱ همارزه (۷ منهای منهای ۱ میشه هشت که باقیماندهاش بر هشت صفره). در واقع اگه دقت کنیم هر عددی رو میشه بر مبنای همارزی، به دو شکل داخل این حلقه وصف کرد. حالت مثبت و حالت منفی. ۶ با منفی ۲ همارزه، ۵ با منفی ۳ و این کار رو میشه تا ۴ انجام داد. به بیان دیگه، با بیان همارزی به پیمانهی ۳، ما دو صورت از اعداد صحیح رو میتونیم نشون بدیم:
$$\{0,1,2,3,4,5,6,7\}$$و
$$\{-4,-3,-2,-1,0,1,2,3\}$$به جای هر عدد بزرگتر یا مساوی چهار در مجموعهی اول، همارزش رو گذاشتیم تو مجموعهی دوم. نمایش بیتی هر کدوم رو داشته باشیم، اینطور چیزی میشه:
10 000
21 001
32 010
43 011
54 or -4: 100
65 or -3: 101
76 or -2: 110
87 or -1: 111
و اینطور ما تونستیم مجموعهی اعداد صحیح علامتدار رو بسازیم (به پیمانهی ۳). به بیان دیگه اگه $x$ داخل مجموعهی اعداد صفر تا $2^n - 1$ تعریف شده باشه، فرمول مجموعهی علامتدار متناظر باهاش این میشه:
$$ signed_n(x) = \begin{cases} x, & x < 2^{n-1} \\ x - 2^n, & x \ge 2^{n-1} \end{cases} $$تنها مشکلی که فرمول بالا برای محاسبه داخل کامپیوتر داره اینه که وقتی ما با n بیت کار میکنیم، مقدار $2^n$ رو نداریم و بزرگترین مقداری که داریم، $2^n -1$ هست که برای درست شدن فرمول باید حالت دوم فرمول رو با یک جمع و تفریق کنیم، پس:
از طرفی چون جمع هر عدد و نقیضش میشه عددی که همهی بیتهاش یک هستن:
$$ 2^n - 1 = 111...1 = x + \sim x \Rightarrow \sim x = (2^n - 1) - x $$دقت کنیم در فرمول بالا، چون عنصر وارون جمعی رو داخل حلقه داریم، پس:
$$ x + y \equiv 0 \pmod{2^n} \Rightarrow y \equiv -x \pmod{2^n}\Rightarrow y \equiv 2^n - x \pmod{2^n} $$این یعنی ما یه راه عملی برای ساختن عدد منفی هم پیدا کردیم، نات عدد رو با یک جمع میکنیم و میشه عدد منفیمون:
$$ comp^2_n(x) = \begin{cases} x, & x < 2^{n-1} \\ \sim x + 1, & x \ge 2^{n-1} \end{cases} $$به این میگن نمایش مکمل ۲ی عدد. در نهایت ما تونستیم نمایش عدد منفی رو خود عدد منفی رو داخل این حلقه پیدا کنیم و چون حلقهست و بستهست، پس بدون اینکه نیاز به چیز دیگهای باشه، جمع هم مثل تفریق داخل این حلقه تعریف میشه و نیاز نیست برای تفریق کار خاص دیگهای بکنیم، مثلاً سه منهای یک (سه + منفی یک) رو در نظر بگیریم:
1 3: 011
2-1: 111
3-------
4 1010
که اگه اون بیت آخری رو حذف کنیم (در واقع اینجا آورفلوی علامتدار رخ داده و فلگ V روشن میشه) نتیجه میشه 010 که همون ۲ هست، چیزی که از این تفریق انتظار داشتیم.
توابع حالتساز
اگه برگردیم به بحثی که داشتیم، حالا علت اینکه اون چهارتا فلگ رو داریم روشنتر میشه. بسته به شکل مقایسهای که از نظر علامتدار بودن یا نبودن ماجرا میخوایم داشته باشیم، ترکیبی از فلگهایی که داشتیم، میتونن بگن که حاصل چی بوده (اگه علامتدار در نظر بگیریم چطور شده حاصل و اگه بدون علامت در نظر گرفته باشیم رجیسترها رو چی شدن). معمولاً در حالت عادی، خیلی کسی کاری به این نداره که الان حالت حاصل از نظر صفر بودن، منفی شدن و آورفلو شدن چی شد. محاسبات انجام میشه و میره. برای حالتی که مهمه (مثلاً میخوایم شرطی رو چک کنیم) یه سری توابع مشخص هستن که علاوه بر کاری که انجام میدادن، باعث پر شدن این فلگها هم میشن:
ADDS,SUBS,ANDSCMN,CMP,TST
دستهی اول که روشنن چی هستن. جمع و تعریف و اند میکنن و بر اساس مقداری که به دست میاد در نهایت، فلگها رو هم ست میکنن.
دستهی دوم هم در واقع یه میانبری هستن برای دستهی اول:
1CMN x1, x2 // <- ADDS xzr, x1, x2 <- xzr = x1+x2
2CMP x1, x2 // <- SUBS xzr, x1, x2 <- xzr = x1-x2
3TST x1, x2 // <- ANDS xzr, x1, x2 <- xzr = x1&x2
اون xzr در واقع رجیستر x31 هست که مثل dev/null/ لینوکس عمل میکنه: نوشتن توش فایده نداره و هر موقع هم ازش بخونی مقدار صفر بر میگردونه.
تنها نکتهای که میمونه اینه که چرا وقتی V داریم، C هم داریم اگه دوتاشون برای آورفلو هستن. فرض کنیم در حال ۳۲ بیت میخوایم 0xFFFFFFFF + 1 رو انجام بدیم. در حالت بدون علامت مقدار 0x100000000 میشه که ۱ در بیت ۳۳ قرار داره و بنابراین مقدار C یک میشه، ولی اگه همین جمع رو در حال علامتدار انجام بدیم عدد 0xFFFFFFFF همون منفی یک هست و حاصل جمع صفر میشه پس آورفلویی رخ نداده و V صفر خواهد بود.
تغییر شرطی جریان برنامه
حالا اگه برگردیم به جایی که بودیم میتونیم برنچ گرفتنهای شرطی رو داشته باشیم:
| حالت | مخفف | شرط معادل | تایپ فرضی رجیسترها | توضیح بر اساس NZCV |
|---|---|---|---|---|
| beq | branch if equal | x0 == x1 | Z == 1 | |
| bne | branch if not equal | x0 != x1 | Z == 0 | |
| bge | branch if greater than or equal | x0 >= x1 | علامتدار | N == V |
| bgt | branch if greater than | x0 > x1 | علامتدار | Z == 0 & N == V |
| ble | branch if less than or equal | x0 <= x1 | علامتدار | Z == 1 | N != V |
| blt | branch if less than | x0 < x1 | علامتدار | N != V |
| bhi | branch if higher | x0 >= x1 | بدون علامت | C == 1 & Z == 0 |
| bhs | branch if higher or same | x0 > x1 | بدون علامت | C == 1 |
| blo | branch if lower | x0 <= x1 | بدون علامت | C == 0 |
| bls | branch if lower or same | x0 < x1 | بدون علامت | C == 0 | Z == 1 |
اجرای اسکی به بایت
حالا در نهایت برای اینکه ببینیم کدی که در بالا نوشتیم درست بوده یا نه، داخل سیسکال write به مقدار بافر خروجی توجه میکنیم:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffd8c5cc70 /* 25 vars */) = 0
3read(0, 123
4"123", 3) = 3
5write(1, 0x31, 1) = -1 EFAULT (Bad address)
6write(1, 0x32, 1) = -1 EFAULT (Bad address)
7write(1, 0x33, 1) = -1 EFAULT (Bad address)
8exit(0) = ?
9+++ exited with 0 +++
سه تا مقدار 0x31، 0x32 و 0x33 همون مقادیر هگز اسکی ۱ و ۲ و ۳ هستن. پس میشه تبدیلشون کنیم به مقدار عددی:
1.global _start
2
3.section .data
4 octet: .space 3
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =octet
11 mov w2, #3
12 svc #0
13
14 ldr x3, =octet
15 mov w4, #3
16 bl str_to_num
17
18 mov w8, #93
19 mov w0, #0
20 svc #0
21
22str_to_num:
23 ldrb w5, [x3]
24 sub w5, w5, #0x30 ; w5 <- w5 - 48 (48 or 0x30 is ascii code of '0')
25
26 mov w8, #64
27 mov w0, #1
28 mov w1, w5
29 mov w2, #1
30 svc #0
31
32 add x3, x3, #1
33
34 subs w4, w4, #1
35
36 bne str_to_num
37
38 ret
و در نهایت میمونه ذخیره کردن هر عدد. برای ذخیره میتونیم ورد صفر رو صفر کنیم. هر بار مقدار قبلیش رو در ۱۰ ضرب کنیم و با رقمی که ساختیمش جمع کنیم:
1.global _start
2
3.section .data
4 octet: .space 3
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =octet
11 mov w2, #3
12 svc #0
13
14 ldr x3, =octet
15 mov w4, #3
16 mov w0, #0
17 bl str_to_num
18
19 mov w8, #64
20 mov w0, #1
21 mov w1, w7
22 mov w2, #1
23 svc #0
24
25 mov w8, #93
26 mov w0, #0
27 svc #0
28
29str_to_num:
30 ldrb w5, [x3]
31 sub w5, w5, #0x30
32
33 mov w8, #10
34 mul w0, w0, w8
35 add w0, w0, w5
36
37 add x3, x3, #1
38
39 subs w4, w4, #1
40
41 bne str_to_num
42
43 ret
و رانش که از پارامتر دوم write قراره مقداری که داریم رو بفهمیم چقدر بوده:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffc8a20af0 /* 25 vars */) = 0
3read(0, 128
4"128", 3) = 3
5write(1, 0x1, 1) = -1 EFAULT (Bad address)
6write(1, 0xc, 1) = -1 EFAULT (Bad address)
7write(1, 0x80, 1) = -1 EFAULT (Bad address)
8exit(0) = ?
9+++ exited with 0 +++
و جمع زدنش هم سخت نیست دیگه:
1.global _start
2
3.section .data
4 octet: .space 3
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =octet
11 mov w2, #3
12 svc #0
13
14 ldr x3, =octet
15 mov w4, #3
16 mov w0, #0
17 bl str_to_num
18 mov w7, w0 // store result of calling function
19
20 add w7, w7, 10 // to end this subsection
21
22 mov w8, #64
23 mov w0, #1
24 mov w1, w7
25 mov w2, #1
26 svc #0
27
28 mov w8, #93
29 mov w0, #0
30 svc #0
31
32str_to_num:
33 ldrb w5, [x3]
34 sub w5, w5, #0x30
35
36 mov w8, #10
37 mul w0, w0, w8
38 add w0, w0, w5
39
40 add x3, x3, #1
41
42 subs w4, w4, #1
43
44 bne str_to_num
45
46 ret
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xfffffa2898a0 /* 25 vars */) = 0
3read(0, 128
4"128", 3) = 3
5write(1, 0x8a, 1) = -1 EFAULT (Bad address)
6exit(0) = ?
7+++ exited with 0 +++
که ۱۲۸ در واقع مقدار عددیش در نمایش مبنای ۱۶ میشه 0x8a. به نظر این تیکه هم تمومه.
یه آیپی CIDR رو بخونه و تبدیلش کنه به یه صحیح ۳۲بیتی و بایت ماسک
ما الان با کد بالا فناوری تبدیل اکتت به مسک رو داریم :)). اینجا میرسیم به خوندن یه آیپی به فرمت 123.123.123.123/12 و طبعاً شرطیها در برنامهنویسی. باید یه رشتهی ماکزیمم ۱۸ کاراکتری رو بخونیم. هر وقت به نقطه رسیدیم تا اونجا رو یه اوکتت در نظر بگیریم، تبدیلش کنیم به عدد، بذاریمش تو جای خودش در یه ورد (اکتت اول میشه ۸ بیت چپ ورد، اکتت دوم میشه ۸ بیت دوم و الی آخر) و در نهایت اگه به اسلش رسیدیم، ماکزیمم دوتا کاراکتر بعدش رو تبدیل کنیم به عدد و بذاریم داخل بایت مسک.
خوندن رو که قبلاً رفتیم:
1.global _start
2
3.section .data
4 cidrIP: .space 18
5
6.section .text
7_start:
8 mov w8, #63
9 mov w0, #0
10 ldr w1, =cidrIP
11 mov w2, #18
12 svc #0
13
14 mov w8, #64
15 mov w0, #1
16 ldr w1, =cidrIP
17 mov w2, #18
18 svc #0
19
20 mov w8, #93
21 mov w0, #0
22 svc #0
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ ./main
2123.123.123.123/12
3123.123.123.123/12
حالا باید تابعی داشته باشیم که به هر نقطه که رسید تیکه کنه ماجرا رو، یچی شبیه
1s.split(".")
داخل جاوااسکریپت مثلاً. حالا احتمالاً به جای باحال ماجرا رسیدیم. میخوایم شروع کنیم به پردازش کردن کاراکتر به کاراکتر رشتهای که گرفتیم و
- اگه عدد بود ذخیرهش کنیم یجایی
- اگه رسیدیم به نقطه، عددی که ذخیره کردیم رو تو جای مناسبی داخل ورد قرار بدیم
- اگه به
/رسیدیم یعنی آیپی تموم شده و به مسک رسیدیم - اگه به کاراکتر نال یا
n\رسیدیم یعنی آخر رشتهست و مسک رو ذخیره کنیم و بره
طبعاً قرار نیست اینا رو پشت سر هم بنویسیم بره و نیاز داریم برای اینکه کدمون حداقلی از خوانایی رو داشته باشه، تابع تعریف کنیم. و خب اینجا قراره چندتا تابع درون هم کال شن و هی برن اینور اونور.
مسئولیت حفظ رجیسترها با کیه؟
نکتهای که اینجا هست اینه که بالاتر گفتیم که هر بار که برنچ با لینک میگیریم آدرس خط بعدی در LR ذخیره میشه و اگه ret رو کال کنیم تو تابع دوم، بر میگردیم به ادامهی اجرای کد از جایی که تابع رو صدا زده بودیم. مشکل الان اینجاست که اگه چندبار برنچ با لینک بگیریم، هر بار رجیستر LR بروز میشه و جایی که بودیم گم میشه.
برای حل این مشکل یه سری قاعدهی مندرآوردی (به معنای اینکه زبان فورسش نمیکنه ولی همه اینطور کد رو مینویسن که کدهاشون با بقیه سازگار باشه یا اگه از کتابخونهای استفاده میکنن مشکلی نیاد) هست که میگه کی مسئولیت حفظ رجیسترها رو به عهده داره. اول اون قواعد رو بررسی میکنیم و بعد میریم سر وقت اینکه برای حل مشکل از دست دادن لینک به جای بعد برنچ گرفتن باید چیکار کرد.
| رجیستر | نقش | مسئولیت حفظ |
|---|---|---|
x0 تا x7 | آرگومان تابع / نتیجه | کسی که کال میکنه |
x8 | پوینتر به نتیجه / سیسکال | کسی که کال میکنه |
x9 تا x15 | ذخیرهسازی موقت | کسی که کال میکنه |
x19 تا x28 | استفادهی عمومی | کسی که کال شده |
x29 (FP) | Frame Pointer | کسی که کال شده |
x30 (LR) | Link Register | کسی که کال شده |
SP | Stack Pointer | کسی که کال شده |
رجیسترهای ۱۶ و ۱۷ و ۱۸ رو نفهمیدم چیا هستن و نیاوردم تو این لیست :)).
در نهایت منظور از ستون سوم اینه که
- اگه داخل تابع اول قراره تابع دوم کال شه و تابع اول به رجیسترهای
x0تاx15نیازه داره، این وظیفهی خود تابع اوله که مقادیر اونا رو ذخیره کنه و تابع دوم وظیفهای نداره که به این مقادیر دست نزنه. - اگه داخل تابع اول قراره تابع دوم کال شه و تابع دوم نیاز داره به رجیسترهای
x19تاx30و همینطور اشارهگر استک، وظیفهاشه که مقادیر این رجیسترها رو حفظ کنه و بعد تغییرشون بده و کارش هم که تموم شد و خواست به تابع اول برگرده، مقادیر اولیه رو بذاره سر جاش و بعد برگرده.
مسئلهی استک
در ادامه یه تیکه کد مینویسیم که دو سطح از کال کردن تابع داشته باشیم که به مشکل از دست دادن LR بخوریم و بر اساس جدول بالا و چیزایی که تا حالا دیدیم، حلش کنیم. این کد همون کارو میکنه:
1.global _start
2
3.section .text
4_start:
5 bl f1
6
7 mov x8, #93
8 mov x0, #0
9 svc #0
10
11f1:
12 bl f2
13 ret
14
15f2:
16 ret
و اگه رانش کنیم میبینیم که همینطور وایمیسه و نگاهمون میکنه. برای اینکه بفهمیم چرا اینطور میشه با gdb میریم برای دیباگش:
1meysam@ubuntu:~/www/test/arm/netmask$ as -g -o ctc.o ctc.s && ld -o ctc ctc.o
2meysam@ubuntu:~/www/test/arm/netmask$ gdb ./ctc
3Reading symbols from ./ctc...
4(gdb) break _start
5Breakpoint 1 at 0x400078: file ctc.s, line 5.
6(gdb) disas /r _start
7Dump of assembler code for function _start:
8 0x0000000000400078 <+0>: 94000004 bl 0x400088 <f1>
9 0x000000000040007c <+4>: d2800ba8 mov x8, #0x5d // #93
10 0x0000000000400080 <+8>: d2800000 mov x0, #0x0 // #0
11 0x0000000000400084 <+12>: d4000001 svc #0x0
12End of assembler dump.
13(gdb) disas /r f1
14Dump of assembler code for function f1:
15 0x0000000000400088 <+0>: 94000002 bl 0x400090 <f2>
16 0x000000000040008c <+4>: d65f03c0 ret
17End of assembler dump.
18(gdb) disas /r f2
19Dump of assembler code for function f2:
20 0x0000000000400090 <+0>: d65f03c0 ret
21End of assembler dump.
22(gdb) run
23Breakpoint 1, _start () at ctc.s:5
245 bl f1
25(gdb) info registers x30
26x30 0x0 0
27(gdb) s
28f1 () at ctc.s:12
2912 bl f2
30(gdb) info registers x30
31x30 0x40007c 4194428
32(gdb) s
33f2 () at ctc.s:16
3416 ret
35(gdb) info registers x30
36x30 0x40008c 4194444
37(gdb) s
38f1 () at ctc.s:13
3913 ret
40(gdb) info registers x30
41x30 0x40008c 4194444
42(gdb) s
43^C
44Program received signal SIGINT, Interrupt.
45f1 () at ctc.s:13
4613 ret
47(gdb) info registers x30
48x30 0x40008c 4194444
بعد از اینکه گیر کرد با کنترل+سی میزنیم تو سرش. تو تیکهی اول دیاسمبل هر تیکه و در آخر هم آمار رجیستر x30 که همون LR هست رو میشه دید. در ادامه برای اشاره به آدرس از دو رقم آخر استفاده میکنیم. با ران کردن برنامه، تابع start_ کال میشه و میرسه به خط ۷۸ و تابع f1 رو کال میکنه، مقدار x30 میشه 7c (یعنی خطی که بعد از برنچلیک داخل start_ هست). تابع f1 تابع f2 رو کال میکنه و مقدارش x30 میشه 8c که میشه خط ret در تابع f1. تابع f2 کال میشه و میرسه به ret خودش و بر میگرده به آدرس 8c. آدرس 8c همون خط ret تابع f1 هست پس تلاش میکنه بره به آدرسی که داخل x30 هست یعنی 8c و اون آدرس خودشه، پس دوباره ret کال میشه و میخواد بره به 8c و این ماجرا تا ابد ادامه پیدا میکنه.
حالا برگردیم به مسئلهمون. فرض کنیم ما تابع ۱ و ۲ و ۳ رو کال میکنیم. انتظار داریم بعد از اتمام ۳، ادامهی تابع ۲ ران شه و بعد از اتمام اون، ادامهی تابع ۱ ران شه. یعنی انتظار ما یه فرآیند LIFOعه و این همون استکه. داخل آرم۶۴ یه پوینتر مخصوص وجود داره به اسم SP که داره همیشه به آخرین فریم استک اشاره میکنه و ما میتونیم ازش برای پوش و پاپ کردن دیتا استفاده کنیم. مشکل ما داخل کد بالا این بود که از با کال شدن تابع سوم و برگشتنش، ما LR رو داخل تابع دوم گم میکردیم و پس الان کافیه این رجیستر رو قبل از رفتن به تابع بعدی ذخیره کنیم و قبل از صدا زدن RET تابع خودمون اون رو برگردونیم. قبل از اینکه بریم و کد رو تغییر بدیم فقط باید به این نکته توجه کنیم که داخل آرم۶۴، استک رو به پایین رشد میکنه (موقع پوش باید آدرس رو کم کرد و موقع پاپ آدرس رو اضافه کرد).
1.global _start
2
3.section .text
4_start:
5 bl f1
6
7 mov x8, #93
8 mov x0, #0
9 svc #0
10
11f1:
12 stp x29, x30, [sp, #-16]!
13 bl f2
14 ldp x29, x30, [sp], #16
15 ret
16
17f2:
18 ret
داخل کد بالا ما x29 رو هم دخیره کردیم. این رجیستر فریم پورینتره (FP) و همیشه به آخرین فریم استک داره اشاره میکنه. واجب نیست بروز کردنش ولی اگه نیازه که از ابزارهای دیباگ استفاده کنیم، باید داشته باشیمش. و رانش:
1meysam@ubuntu:~/www/test/arm/netmask$ gdb ./ctc
2Reading symbols from ./ctc...
3(gdb) break _start
4Breakpoint 1 at 0x400078: file ctc.s, line 5.
5(gdb) run
6Starting program: /home/meysam/www/test/arm/netmask/ctc
7Breakpoint 1, _start () at ctc.s:5
85 bl f1
9(gdb) info registers x30
10x30 0x0 0
11(gdb) s
12f1 () at ctc.s:12
1312 stp x29, x30, [sp, #-16]!
14(gdb) s
1513 bl f2
16(gdb) s
17f2 () at ctc.s:18
1818 ret
19(gdb) info registers x30
20x30 0x400090 4194448
21(gdb) s
22f1 () at ctc.s:14
2314 ldp x29, x30, [sp], #16
24(gdb) info registers x30
25x30 0x400090 4194448
26(gdb) s
2715 ret
28(gdb) info registers x30
29x30 0x40007c 4194428
30(gdb) s
31_start () at ctc.s:7
327 mov x8, #93
33(gdb) info registers x30
34x30 0x40007c 4194428
35(gdb) s
368 mov x0, #0
37(gdb)
389 svc #0
39(gdb)
40[Inferior 1 (process 79060) exited normally]
از این به بعد پس طبق اون جدول هر تابعی که مسئولیتش حفظ یه رجیستر باشه میتونه از استک برای این کار استفاده کنه.
پردازش IP
برگردیم به مسئلهی خودمون. اول تلاش میکنیم یه رشتهی ۱۵ کاراکتری بخونیم و تبدیلش کنیم به یه عدد صحیح ۳۲ بیتی. یعنی ۸ بیت رو که خوندیم و به نقطه رسیدیم، ۸ بیت شیفت میدیم به چپ و میریم سراغ بعدی. از اینجا به بعد هم تلاش میکنیم که از اون جدول و نقشهایی که رجیسترها دارن استفاده کنیم.
1.global _start
2
3.section .data
4 cidrIP: .space 15
5 ip: .word 0
6
7.section .text
8_start:
9 // read from input
10 mov w8, #63
11 mov w0, #0
12 ldr x1, =cidrIP
13 mov w2, #15
14 svc #0
15
16 mov x0, x1
17 bl process
18
19 // print
20 mov w8, #64
21 mov w0, #1
22 ldr w1, =ip
23 mov w2, #4
24 svc #0
25
26 // exit
27 mov w8, #93
28 mov w0, #0
29 svc #0
30
31process:
32 stp x29, x30, [sp, #-16]!
33 mov w10, #0 // temp octet
34 loop:
35 ldrb w11, [x0], #1
36 cmp w11, #'.'
37 beq store_and_loop
38 cmp w11, #'\n'
39 beq store_and_exit
40 cmp w11, #'\0'
41 beq store_and_exit
42 bl char_to_num
43 b loop
44
45store_and_exit:
46 bl store
47 ldp x29, x30, [sp], #16
48 ret
49
50store_and_loop:
51 bl store
52 b loop
53
54store:
55 ldr x14, =ip
56 ldr w15, [x14]
57 lsl w15, w15, #8
58 orr w15, w15, w10
59 mov w10, #0
60 str w15, [x14]
61 ret
62
63char_to_num:
64 sub w12, w11, #'0'
65 mov w13, #10
66 mul w10, w10, w13
67 add w10, w10, w12
68 ret
اگه برنامه رو ران کنیم و 65.66.67.68 رو به عنوان آیپی (هر اکتت میشه اسکی حروف انگلیسی اول تا چهارم) بهش بدیم، نتیجه میشه این:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffe791dcf0 /* 26 vars */) = 0
3read(0, 65.66.67.68
4"65.66.67.68\n", 15) = 12
5write(1, "DCBA", 4DCBA) = 4
6exit(0) = ?
7+++ exited with 0 +++
که همونطور که مشخصه رشتهی DCBA رو چاپ میکنه که نتیجه میده پردازش ما درست بوده.
چاب IP
قبل از اینکه ادامه بدیم یه فانکشن مینویسیم که بتونیم باهاش یه ورد رو به صورت رشتهی IP چاپ کنیم که کارمون برای جلوتر رفتن راحتتر شه. فرآیند هم برعکس چیزیه که در تبدیل اسکی به بایت اتفاق افتاد. هر بایت رو میخونیم، هر رقمش رو با کاراکتر صفر جمع میزنیم و بین هر سه کاراکتر هم یه کاراکتر نقطه میذاریم.
1.global _start
2
3.section .data
4 ip_str: .space 16
5
6.section .text
7_start:
8 movz w0, #0x0001 // 0.1
9 movk w0, #0x7f00, lsl #16 // 127.0.0.1
10 bl print_as_ip
11 mov w8, #93
12 mov w0, #0
13 svc #0
14
15print_as_ip:
16 stp x29, x30, [sp, #-16]!
17 ldr x1, =ip_str
18 bl word_to_ip
19 ldr x0, =ip_str
20 bl str_len
21 mov w2, w0
22 mov w8, #64
23 mov w0, #1
24 ldr x1, =ip_str
25 svc #0
26 ldp x29, x30, [sp], #16
27 ret
28
29word_to_ip:
30 stp x29, x30, [sp, #-16]!
31 mov x7, x1
32 mov w2, #0xFF
33 start_word_to_ip:
34 and w3, w0, w2 // get the first octet
35 lsr w0, w0, #8
36 mov w4, #10
37 loop_word_to_ip:
38 udiv w5, w3, w4 // w5 <- w3 / 10 <- unsigned division
39 mov w6, w5 // temp for future check
40 msub w5, w5, w4, w3 // w5 <- w3 - ((w3 / 10) * 10) <- reminder
41 mov w3, w6
42 add w5, w5, #'0'
43 strb w5, [x1], #1
44 cbz w6, add_dot
45 b loop_word_to_ip
46 add_dot:
47 cbz w0, done_word_to_ip // if there is no octet remained, return
48 mov w3, #'.'
49 strb w3, [x1], #1
50 b start_word_to_ip
51 done_word_to_ip:
52 mov x0, x7
53 mov x7, x1
54 sub x1, x1, #1
55 bl rev_string
56 mov w5, #'\n'
57 strb w5, [x7]
58 ldp x29, x30, [sp], #16
59 ret
60
61rev_string:
62 start_rev_string:
63 cmp x0, x1
64 bge end_rev_string
65 ldrb w2, [x0]
66 ldrb w3, [x1]
67 strb w2, [x1]
68 strb w3, [x0]
69 add x0, x0, #1
70 sub x1, x1, #1
71 b start_rev_string
72 end_rev_string:
73 ret
74
75str_len:
76 stp x29, x30, [sp, #-16]!
77 mov w1, 0
78 start_str_len:
79 ldrb w2, [x0]
80 cmp w2, #0x00
81 beq end_str_len
82 cmp w2, #'\n'
83 beq end_with_lf
84 add w1, w1, #1
85 add x0, x0, #1
86 b start_str_len
87 end_with_lf:
88 add w1, w1, #1
89 end_str_len:
90 mov w0, w1
91 ldp x29, x30, [sp], #16
92 ret
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./word_to_ip
2execve("./word_to_ip", ["./word_to_ip"], 0xffffe4bf9ab0 /* 26 vars */) = 0
3write(1, "127.0.0.1\n", 10127.0.0.1
4) = 10
5exit(0) = ?
6+++ exited with 0 +++
البته قرار بود این کار رو تو بخش آخر انجام بدیم ولی خب الان نیاز بود برای پیش رفتن :)). و در نهایت گرفتن IP از ورودی و چاپش:
1.global _start
2
3.section .data
4 cidrIP: .space 18
5 ip: .word 0
6 mask: .byte 0
7
8.section .text
9_start:
10 // read from input
11 mov w8, #63
12 mov w0, #0
13 ldr x1, =cidrIP
14 mov w2, #15
15 svc #0
16
17 mov x0, x1
18 bl process
19
20 // print
21 bl print
22
23 // exit
24 mov w8, #93
25 mov w0, #0
26 svc #0
27
28print:
29 stp x29, x30, [sp, #-16]!
30 ldr x0, =ip
31 bl print_as_ip
32 ldp x29, x30, [sp], #16
33 ret
34
35print_as_ip:
36 stp x29, x30, [sp, #-16]!
37 mov x9, x0
38 mov x1, x0
39 ldr x0, [x0]
40 bl word_to_ip
41 mov x0, x9
42 bl str_len
43 mov w2, w0
44 mov w8, #64
45 mov w0, #1
46 mov x1, x9
47 svc #0
48 ldp x29, x30, [sp], #16
49 ret
50
51process:
52 stp x29, x30, [sp, #-16]!
53 mov w10, #0 // temp octet
54 ldr x14, =ip
55 mov w16, #8
56 loop:
57 ldrb w11, [x0], #1
58 cmp w11, #'.'
59 beq store_and_loop
60 cmp w11, #'/'
61 beq update_to_store_mask
62 cmp w11, #'\n'
63 beq store_and_exit
64 cmp w11, #'\0'
65 beq store_and_exit
66 bl char_to_num
67 b loop
68
69store_and_exit:
70 bl store
71 ldp x29, x30, [sp], #16
72 ret
73
74store_and_loop:
75 bl store
76 b loop
77
78store:
79 ldr w15, [x14]
80 lsl w15, w15, #8
81 orr w15, w15, w10
82 mov w10, #0
83 str w15, [x14]
84 ret
85
86char_to_num:
87 sub w12, w11, #'0'
88 mov w13, #10
89 mul w10, w10, w13
90 add w10, w10, w12
91 ret
92
93update_to_store_mask:
94 bl store
95 ldr x14, =mask
96 mov w16, #4
97 mov w10, #0
98 b loop
99
100word_to_ip:
101 stp x29, x30, [sp, #-16]!
102 mov x7, x1
103 mov w2, #0xFF
104 start_word_to_ip:
105 and w3, w0, w2 // get the first octet
106 lsr w0, w0, #8
107 mov w4, #10
108 loop_word_to_ip:
109 udiv w5, w3, w4 // w5 <- w3 / 10 <- unsigned division
110 mov w6, w5 // temp for future check
111 msub w5, w5, w4, w3 // w5 <- w3 - ((w3 / 10) * 10) <- reminder
112 mov w3, w6
113 add w5, w5, #'0'
114 strb w5, [x1], #1
115 cbz w6, add_dot
116 b loop_word_to_ip
117 add_dot:
118 cbz w0, done_word_to_ip // if there is no octet remained, return
119 mov w3, #'.'
120 strb w3, [x1], #1
121 b start_word_to_ip
122 done_word_to_ip:
123 mov x0, x7
124 mov x7, x1
125 sub x1, x1, #1
126 bl rev_string
127 mov w5, #'\n'
128 strb w5, [x7]
129 ldp x29, x30, [sp], #16
130 ret
131
132rev_string:
133 start_rev_string:
134 cmp x0, x1
135 bge end_rev_string
136 ldrb w2, [x0]
137 ldrb w3, [x1]
138 strb w2, [x1]
139 strb w3, [x0]
140 add x0, x0, #1
141 sub x1, x1, #1
142 b start_rev_string
143 end_rev_string:
144 ret
145
146str_len:
147 stp x29, x30, [sp, #-16]!
148 mov w1, 0
149 start_str_len:
150 ldrb w2, [x0]
151 cmp w2, #0x00
152 beq end_str_len
153 cmp w2, #'\n'
154 beq end_with_lf
155 add w1, w1, #1
156 add x0, x0, #1
157 b start_str_len
158 end_with_lf:
159 add w1, w1, #1
160 end_str_len:
161 mov w0, w1
162 ldp x29, x30, [sp], #16
163 ret
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ strace ./main
2execve("./main", ["./main"], 0xffffe6bcc340 /* 26 vars */) = 0
3read(0, 65.66.67.68/18
4"65.66.67.68/18\n", 15) = 15
5write(1, "65.66.67.68\n", 1265.66.67.68
6) = 12
7exit(0) = ?
8+++ exited with 0 +++
نتمسک رو بسازه (و بقیهی IPها رو)
روزی که شروع کردم ۱۵ اسفند بود و قرار بود تو این بخش فقط نتمسک رو بسازیم و بعد در بخش بعد بقیهی آیپیها رو بسازیم، خیلی گنگ بود برام که قراره به کجا بریم و اینطور شکسته بودم مسئله رو و الان که ۲۱ اسفنده میبینم دیگه چیزی نمونده. پس تو همین بخش هر چهارتا آیپی رو میسازیم و چاپ میکنیم. از روی بخش بعدی در دستهبندی میپریم و میریم برای خوشگل چاپ کردن آدرسها در قسمت بعد.
چیزی برای توضیح نمونده. مسک رو که پردازش کردیم و داریم، یه ورد رو نقیض میکنیم که همهی بیتهاش بشن یک، به اندازهی ۳۲ منهای مسک شیفت میدیم به چپ میشه نتمسکمون. اند با آدرس میشه نتورکآدرس، نقیض نتمسک رو با آدرس یا میکنیم میشه برودکست آدرس و از دوتای آخری هم مثبت منفی یک میشه اولین و آخرین آدرسهای قابل استفادهی شبکه.
1.global _start
2
3.section .data
4 cidrIP: .space 18
5 ip: .space 15
6 mask: .byte 0
7 netmask: .space 15
8 network: .space 15
9 broadcast: .space 15
10 first_usable: .space 15
11 last_usable: .space 15
12
13.section .text
14_start:
15 // read from input
16 mov w8, #63
17 mov w0, #0
18 ldr x1, =cidrIP
19 mov w2, #15
20 svc #0
21
22 mov x0, x1
23 bl process
24 bl calculate_netmask
25 bl calculate_addresses
26
27 // print
28 bl print
29
30 // exit
31 mov w8, #93
32 mov w0, #0
33 svc #0
34
35print:
36 stp x29, x30, [sp, #-16]!
37 ldr x0, =ip
38 bl print_as_ip
39 ldr x0, =network
40 bl print_as_ip
41 ldr x0, =netmask
42 bl print_as_ip
43 ldr x0, =broadcast
44 bl print_as_ip
45 ldr x0, =first_usable
46 bl print_as_ip
47 ldr x0, =last_usable
48 bl print_as_ip
49 ldp x29, x30, [sp], #16
50 ret
51
52print_as_ip:
53 stp x29, x30, [sp, #-16]!
54 mov x9, x0
55 mov x1, x0
56 ldr x0, [x0]
57 bl word_to_ip
58 mov x0, x9
59 bl str_len
60 mov w2, w0
61 mov w8, #64
62 mov w0, #1
63 mov x1, x9
64 svc #0
65 ldp x29, x30, [sp], #16
66 ret
67
68calculate_netmask:
69 ldr x0, =mask
70 ldrb w1, [x0]
71 mov w3, #32
72 sub w1, w3, w1
73 mov w3, #0
74 mvn w3, w3
75 lsl w3, w3, w1
76 ldr x2, =netmask
77 str w3, [x2]
78 ret
79
80calculate_addresses:
81 ldr x0, =netmask
82 ldr w0, [x0]
83 ldr x1, =ip
84 ldr w1, [x1]
85 mov w2, w1
86 and w2, w2, w0 // network = netmask & ip
87 ldr x3, =network
88 str w2, [x3]
89 add w2, w2, #1
90 ldr x3, =first_usable
91 str w2, [x3]
92 mov w2, w1
93 mvn w3, w0
94 orr w3, w3, w1
95 ldr x2, =broadcast
96 str w3, [x2]
97 sub w3, w3, #1
98 ldr x2, =last_usable
99 str w3, [x2]
100 ret
101
102process:
103 stp x29, x30, [sp, #-16]!
104 mov w10, #0 // temp octet
105 ldr x14, =ip
106 mov w16, #8
107 loop:
108 ldrb w11, [x0], #1
109 cmp w11, #'.'
110 beq store_and_loop
111 cmp w11, #'/'
112 beq update_to_store_mask
113 cmp w11, #'\n'
114 beq store_and_exit
115 cmp w11, #'\0'
116 beq store_and_exit
117 bl char_to_num
118 b loop
119
120store_and_exit:
121 bl store
122 ldp x29, x30, [sp], #16
123 ret
124
125store_and_loop:
126 bl store
127 b loop
128
129store:
130 ldr w15, [x14]
131 lsl w15, w15, #8
132 orr w15, w15, w10
133 mov w10, #0
134 str w15, [x14]
135 ret
136
137char_to_num:
138 sub w12, w11, #'0'
139 mov w13, #10
140 mul w10, w10, w13
141 add w10, w10, w12
142 ret
143
144update_to_store_mask:
145 bl store
146 ldr x14, =mask
147 mov w16, #4
148 mov w10, #0
149 b loop
150
151word_to_ip:
152 stp x29, x30, [sp, #-16]!
153 mov x7, x1
154 mov w2, #0xFF
155 start_word_to_ip:
156 and w3, w0, w2 // get the first octet
157 lsr w0, w0, #8
158 mov w4, #10
159 loop_word_to_ip:
160 udiv w5, w3, w4 // w5 <- w3 / 10 <- unsigned division
161 mov w6, w5 // temp for future check
162 msub w5, w5, w4, w3 // w5 <- w3 - ((w3 / 10) * 10) <- reminder
163 mov w3, w6
164 add w5, w5, #'0'
165 strb w5, [x1], #1
166 cbz w6, add_dot
167 b loop_word_to_ip
168 add_dot:
169 cbz w0, done_word_to_ip // if there is no octet remained, return
170 mov w3, #'.'
171 strb w3, [x1], #1
172 b start_word_to_ip
173 done_word_to_ip:
174 mov x0, x7
175 mov x7, x1
176 sub x1, x1, #1
177 bl rev_string
178 mov w5, #'\n'
179 strb w5, [x7]
180 ldp x29, x30, [sp], #16
181 ret
182
183rev_string:
184 start_rev_string:
185 cmp x0, x1
186 bge end_rev_string
187 ldrb w2, [x0]
188 ldrb w3, [x1]
189 strb w2, [x1]
190 strb w3, [x0]
191 add x0, x0, #1
192 sub x1, x1, #1
193 b start_rev_string
194 end_rev_string:
195 ret
196
197str_len:
198 stp x29, x30, [sp, #-16]!
199 mov w1, 0
200 start_str_len:
201 ldrb w2, [x0]
202 cmp w2, #0x00
203 beq end_str_len
204 cmp w2, #'\n'
205 beq end_with_lf
206 add w1, w1, #1
207 add x0, x0, #1
208 b start_str_len
209 end_with_lf:
210 add w1, w1, #1
211 end_str_len:
212 mov w0, w1
213 ldp x29, x30, [sp], #16
214 ret
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ ./main
265.66.67.68/23
365.66.67.68
465.66.66.0
5255.255.254.0
665.66.67.255
765.66.66.1
865.66.67.254
کد نکتهی خاصی نداره واقعاً. لاجرم برای افزایش هیجان میریم خروجی رو قشنگ کنیم.
چاپ کنیم حاصل رو
حاصل که چاپ شد تو مرحلهی قبلی. اینجا بیایم خوشگلش کنیم. چیزی که دارم فکر میکنم اینه که قبل از چاپ هر کدوم یه رشته بیاد بگه که چیه (و طبیعتاً باید لیترال تعریف کنیم ولی میخوام طولش داینامیک حساب شه) و اینکه دارم فکر میکنم باحاله اگه رنگ رو هم وارد ماجرا کنیم. فعلاً بریم برای چاپ برچسب هر کدوم از آیپیها.
چاپ برچسب برای آدرسها
طبیعتاً برچسبها رو که باید لیترال تعریف کنیم، ولی چون تابع طول رشته رو قبلاً تعریف کردیم، از اون استفاده میکنیم که طول رشته درست حساب شد داخل سیسکال رایت. قبلش یه مثال رو داشته باشیم:
1.global _start
2
3.section .rodata
4 cidrIP_label: .ascii "Please enter IP/mask: \0"
5 ip_label: .ascii "IP: \0"
6 netmask_label: .ascii "Netmask: \0"
7 network_label: .ascii "Subnet: \0"
8 broadcast_label: .ascii "Broadcast: \0"
9 first_usable_label: .ascii "First usable address: \0"
10 last_usable_label: .ascii "Last usable address: \0"
11
12.section .data
13 cidrIP: .space 18
14 ip: .space 15
15 mask: .byte 0
16 netmask: .space 15
17 network: .space 15
18 broadcast: .space 15
19 first_usable: .space 15
20 last_usable: .space 15
21
22.section .text
23_start:
24 ldr x0, =cidrIP_label
25 bl print_label
26 mov w8, #63
27 mov w0, #0
28 ldr x1, =cidrIP
29 mov w2, #15
30 svc #0
31
32 mov x0, x1
33 bl process
34 bl calculate_netmask
35 bl calculate_addresses
36
37 // print
38 bl print
39
40 // exit
41 mov w8, #93
42 mov w0, #0
43 svc #0
44
45print:
46 stp x29, x30, [sp, #-16]!
47 ldr x0, =ip_label
48 bl print_label
49 ldr x0, =ip
50 bl print_as_ip
51 ldr x0, =netmask_label
52 bl print_label
53 ldr x0, =netmask
54 bl print_as_ip
55 ldr x0, =network_label
56 bl print_label
57 ldr x0, =network
58 bl print_as_ip
59 ldr x0, =broadcast_label
60 bl print_label
61 ldr x0, =broadcast
62 bl print_as_ip
63 ldr x0, =first_usable_label
64 bl print_label
65 ldr x0, =first_usable
66 bl print_as_ip
67 ldr x0, =last_usable_label
68 bl print_label
69 ldr x0, =last_usable
70 bl print_as_ip
71 ldp x29, x30, [sp], #16
72 ret
73
74print_label:
75 stp x29, x30, [sp, #-16]!
76 mov w8, #64
77 mov x9, x0
78 bl str_len
79 mov w2, w0
80 mov x1, x9
81 mov w0, #1
82 svc #0
83 ldp x29, x30, [sp], #16
84 ret
85
86print_as_ip:
87 stp x29, x30, [sp, #-16]!
88 mov x9, x0
89 mov x1, x0
90 ldr x0, [x0]
91 bl word_to_ip
92 mov x0, x9
93 bl str_len
94 mov w2, w0
95 mov w8, #64
96 mov w0, #1
97 mov x1, x9
98 svc #0
99 ldp x29, x30, [sp], #16
100 ret
101
102calculate_netmask:
103 ldr x0, =mask
104 ldrb w1, [x0]
105 mov w3, #32
106 sub w1, w3, w1
107 mov w3, #0
108 mvn w3, w3
109 lsl w3, w3, w1
110 ldr x2, =netmask
111 str w3, [x2]
112 ret
113
114calculate_addresses:
115 ldr x0, =netmask
116 ldr w0, [x0]
117 ldr x1, =ip
118 ldr w1, [x1]
119 mov w2, w1
120 and w2, w2, w0 // network = netmask & ip
121 ldr x3, =network
122 str w2, [x3]
123 add w2, w2, #1
124 ldr x3, =first_usable
125 str w2, [x3]
126 mov w2, w1
127 mvn w3, w0
128 orr w3, w3, w1
129 ldr x2, =broadcast
130 str w3, [x2]
131 sub w3, w3, #1
132 ldr x2, =last_usable
133 str w3, [x2]
134 ret
135
136process:
137 stp x29, x30, [sp, #-16]!
138 mov w10, #0 // temp octet
139 ldr x14, =ip
140 mov w16, #8
141 loop:
142 ldrb w11, [x0], #1
143 cmp w11, #'.'
144 beq store_and_loop
145 cmp w11, #'/'
146 beq update_to_store_mask
147 cmp w11, #'\n'
148 beq store_and_exit
149 cmp w11, #'\0'
150 beq store_and_exit
151 bl char_to_num
152 b loop
153
154store_and_exit:
155 bl store
156 ldp x29, x30, [sp], #16
157 ret
158
159store_and_loop:
160 bl store
161 b loop
162
163store:
164 ldr w15, [x14]
165 lsl w15, w15, #8
166 orr w15, w15, w10
167 mov w10, #0
168 str w15, [x14]
169 ret
170
171char_to_num:
172 sub w12, w11, #'0'
173 mov w13, #10
174 mul w10, w10, w13
175 add w10, w10, w12
176 ret
177
178update_to_store_mask:
179 bl store
180 ldr x14, =mask
181 mov w16, #4
182 mov w10, #0
183 b loop
184
185word_to_ip:
186 stp x29, x30, [sp, #-16]!
187 mov x7, x1
188 mov w2, #0xFF
189 start_word_to_ip:
190 and w3, w0, w2 // get the first octet
191 lsr w0, w0, #8
192 mov w4, #10
193 loop_word_to_ip:
194 udiv w5, w3, w4 // w5 <- w3 / 10 <- unsigned division
195 mov w6, w5 // temp for future check
196 msub w5, w5, w4, w3 // w5 <- w3 - ((w3 / 10) * 10) <- reminder
197 mov w3, w6
198 add w5, w5, #'0'
199 strb w5, [x1], #1
200 cbz w6, add_dot
201 b loop_word_to_ip
202 add_dot:
203 cbz w0, done_word_to_ip // if there is no octet remained, return
204 mov w3, #'.'
205 strb w3, [x1], #1
206 b start_word_to_ip
207 done_word_to_ip:
208 mov x0, x7
209 mov x7, x1
210 sub x1, x1, #1
211 bl rev_string
212 mov w5, #'\n'
213 strb w5, [x7]
214 ldp x29, x30, [sp], #16
215 ret
216
217rev_string:
218 start_rev_string:
219 cmp x0, x1
220 bge end_rev_string
221 ldrb w2, [x0]
222 ldrb w3, [x1]
223 strb w2, [x1]
224 strb w3, [x0]
225 add x0, x0, #1
226 sub x1, x1, #1
227 b start_rev_string
228 end_rev_string:
229 ret
230
231str_len:
232 stp x29, x30, [sp, #-16]!
233 mov w1, 0
234 start_str_len:
235 ldrb w2, [x0]
236 cmp w2, #0x00
237 beq end_str_len
238 cmp w2, #'\n'
239 beq end_with_lf
240 add w1, w1, #1
241 add x0, x0, #1
242 b start_str_len
243 end_with_lf:
244 add w1, w1, #1
245 end_str_len:
246 mov w0, w1
247 ldp x29, x30, [sp], #16
248 ret
و اجراش:
1meysam@ubuntu:~/www/test/arm/netmask$ ./main
2Please enter IP/mask: 23.24.25.26/14
3IP: 23.24.25.26
4Netmask: 255.252.0.0
5Subnet: 23.24.0.0
6Broadcast: 23.27.255.255
7First usable address: 23.24.0.1
8Last usable address: 23.27.255.254
الان نمیدونم چیو رنگی پنگی کنم، اگه به ذهنم رسید اینجا رو کامل میکنم.
کد چجوری داخل CPU ران میشه؟
به نظرم این رو انگشتنکرده نباید تموم کنیم ماجرا رو. فرض کنیم میخوایم بفهمیم این کد بعد از اینکه ما اجراش کردیم، چه اتفاقی براش میفته:
1.global _start
2
3_start:
4 mov x0, #128
5 add x2, x1, x0
بعد از اینکه کد رو با as -o ldr.o ldr.s (فرض کنیم اسم فایلمون ldr.s هست) کامپایل کردیم، یه فایل آبجکت داریم (که در مرحلهی بعد از اون استفاده میکنیم و لینک پینکها رو با ld انجام میدیم و فایل اجرایی رو میسازیم) که با استفاده از دستور objdump میشه محتویاتش رو دید:
1meysam@ubuntu:~/www/test/arm/netmask$ objdump -D ldr.o
2
3ldr.o: file format elf64-littleaarch64
4
5
6Disassembly of section .text:
7
80000000000000000 <_start>:
9 0: d2801000 mov x0, #0x80 // #128
10 4: 8b000022 add x2, x1, x0
تو قسمت آخر، هر خط اسمبلیای که نوشته بودیم، نوشته شده: در ستون اول آفست اومده (بایت صفر خط اول، بایت ۴ خط دوم و الی آخر. یادمونه که هر دستور آرم۶۴ اندازهش ۳۲بیته یا ۴ بایت)، بعدش یه عدد هگز اومده و بعدش دستوری که میخواستیم تا ران شه. در نهایت آخر خط هم اگه لیترالی داشتیم به صورت کامنت نشون داده شده. قسمت مهم ماجرا برای عنوان این زیربخش در واقع اون عددهای هگزن. و اگه تبدیلشون کنیم به باینری اینطور چیزی میشن:
1meysam@ubuntu:~/www/test/arm/netmask$ echo "obase=2; ibase=16; D2801000" | bc | sed 's/.\{4\}/& /g'
21101 0010 1000 0000 0001 0000 0000 0000
3meysam@ubuntu:~/www/test/arm/netmask$ echo "obase=2; ibase=16; 8B000022" | bc | sed 's/.\{4\}/& /g'
41000 1011 0000 0000 0000 0000 0010 0010
که اگه اون داکیومنت ISR رو ببینیم (صفحه ۵۲۸)، قالب دستور movz اینطور چیزیه:
1 3 2 1 0
2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
3+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4|s|1 0|1 0 0 1 0 1|h w| imm16 | |
5+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
که پترن مچ میشه با اون چیزی که برای نمایش باینریمون هست (از بیت ۳۰ تا ۲۳ که بیتهای هاردکد شده هستن و طبعاً ثابتن):
1 3 2 1 0
2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
3+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4|s|1 0|1 0 0 1 0 1|h w| imm16 | RD |
5+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6|1|1 0|1 0 0 1 0 1|0 0|0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0|0 0 0 0 0|
7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
همونطور که مشخصه imm16 که مقدارش ۱۲۸ هست (بیت ۷) و RD هم که شمارهی رجیستر رو مشخص میکنه صفر هست، بیت ۳۱ هم چون ۱عه یعنی رجیستر ما ۶۴بیتیه.
برای ورد دومی هم ماجرا به همین منواله (چون میدونیم ADD هست دستور، تو داکیومنت برای پترنش دنبال این دستور میگردیم، صفحهی ۲۰):
1 3 2 1 0
2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
3+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4|s|0|0|0 1 0 1 1|sft|0| Rm | imm6 | Rn | Rd |
5+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6|1|0|0|0 1 0 1 1|0 0|0|0 0 0 0 0|0 0 0 0 0 0|0 0 0 0 1|0 0 0 1 0|
7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
در این مورد هم بیت ۳۱ مشخص میکنه که با ۶۴ بیت رجیستر کار داریم، رجیستر مقصدمون در رنج ۰ تا ۴ هست که میشه X2، عملوند اول در رنج ۵ تا ۹ هست که میشه X1 و در نهایت عملوند دوم هم در رنج ۱۶ تا ۲۰ هستن که مقدارش صفره و میشه X0.
پس با داشتن این ۳۲بیتها میشه برنامهمون رو ران کرد. پس حالا برای اینکه بفهمیم بعد از ساختن فایل اجرایی با ld -o ldr ldr.o چه اتفاقی سر ELF (مخفف Executable Linux File) چی میاد، اون رو باز میکنیم که ببینیم چی توشه:
1meysam@ubuntu:~/www/test/arm/netmask$ readelf -a ./ldr
2ELF Header:
3 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
4 Class: ELF64
5 Data: 2s complement, little endian
6 Version: 1 (current)
7 OS/ABI: UNIX - System V
8 ABI Version: 0
9 Type: EXEC (Executable file)
10 Machine: AArch64
11 Version: 0x1
12 Entry point address: 0x400078
13 Start of program headers: 64 (bytes into file)
14 Start of section headers: 520 (bytes into file)
15 Flags: 0x0
16 Size of this header: 64 (bytes)
17 Size of program headers: 56 (bytes)
18 Number of program headers: 1
19 Size of section headers: 64 (bytes)
20 Number of section headers: 5
21 Section header string table index: 4
22
23Section Headers:
24 [Nr] Name Type Address Offset
25 Size EntSize Flags Link Info Align
26 [ 0] NULL 0000000000000000 00000000
27 0000000000000000 0000000000000000 0 0 0
28 [ 1] .text PROGBITS 0000000000400078 00000078
29 0000000000000008 0000000000000000 AX 0 0 4
30 [ 2] .symtab SYMTAB 0000000000000000 00000080
31 0000000000000120 0000000000000018 3 4 8
32 [ 3] .strtab STRTAB 0000000000000000 000001a0
33 0000000000000044 0000000000000000 0 0 1
34 [ 4] .shstrtab STRTAB 0000000000000000 000001e4
35 0000000000000021 0000000000000000 0 0 1
36Key to Flags:
37 W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
38 L (link order), O (extra OS processing required), G (group), T (TLS),
39 C (compressed), x (unknown), o (OS specific), E (exclude),
40 D (mbind), p (processor specific)
41
42There are no section groups in this file.
43
44Program Headers:
45 Type Offset VirtAddr PhysAddr
46 FileSiz MemSiz Flags Align
47 LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
48 0x0000000000000080 0x0000000000000080 R E 0x10000
49
50 Section to Segment mapping:
51 Segment Sections...
52 00 .text
53
54There is no dynamic section in this file.
55
56There are no relocations in this file.
57
58The decoding of unwind sections for machine type AArch64 is not currently supported.
59
60Symbol table '.symtab' contains 12 entries:
61 Num: Value Size Type Bind Vis Ndx Name
62 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
63 1: 0000000000400078 0 SECTION LOCAL DEFAULT 1 .text
64 2: 0000000000000000 0 FILE LOCAL DEFAULT ABS ldr.o
65 3: 0000000000400078 0 NOTYPE LOCAL DEFAULT 1 $x
66 4: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 _bss_end__
67 5: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 __bss_start__
68 6: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 __bss_end__
69 7: 0000000000400078 0 NOTYPE GLOBAL DEFAULT 1 _start
70 8: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 __bss_start
71 9: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 __end__
72 10: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 _edata
73 11: 0000000000410080 0 NOTYPE GLOBAL DEFAULT 1 _end
74
75No version information found in this file.
اگه به قسمت Section Headers نگاه بندازیم میتونیم قسمت اجرایی کد رو که همون text. هست پیدا کنیم:
1 [ 1] .text PROGBITS 0000000000400078 00000078
2 0000000000000008 0000000000000000 AX 0 0 4
تو این قسمت گفته که آفست این بخش 00000078 در فایل اجرایی هست و طولش هم 0000000000000008 بایت (اون هشت آخری با بقیه صفرا قاطی نشه :دی) هست و فلگ اجرایی X رو همداره. و اگه این قسمت از فایل اجرایی رو بخونیم محتویاتش اینه (با فلگ g گفتیم تو دستههای ۴ بایتی گروهبندی کنه موقع خوندن):
1meysam@ubuntu:~/www/test/arm/netmask$ xxd -g 4 -s 0x00000078 -l 8 ./ldr
200000078: 001080d2 2200008b ...."...
همینطور در اطلاعات اول فایل اومده که فایل به صورت 2s complement, little endian اینکود شده. لیتل اندین یعنی چینش راست به چپ بایتهای پراهمیت. پس اگه ۴ بایت ۴ بایت این رشته رو بخونیم و به صورت آدموار بنویسیم به این دوتا عدد هگز میرسیم:
10xD2_80_10_00
20x8B_00_00_22
در بالا هشت بیت هشت بیت با اندراسکور ماجرا رو جدا کردم که بشه خوندش. و اگه به بالا جایی که فایل آبجکت رو باز کردیم برگردیم، میبینیم این همون دوتا ۴ بیت اجرایی ما هستن که در بالاتر تهشون رو در آوردیم. میمونه اونجا که لینوکس چطور میفهمه این رو ران کنه. باز اگه به اطلاعاتی که در مورد فایل اجرایی گرفتیم برگردیم، میبینیم که مقدار Entry point address مقدارش 0x400078 هست که همون مقدار Address در Section Headersعه.
خوب شد دیگه. برنامه رو که اجرا میکنیم، لینوکس براش یه جدول مجازی حافظه میسازه (یچی مثل mmap برا مپ کردن فایل روی مموری که MMU داخل CPU انجامش میده) بین فضای اجرا و مموری فیزیکال (که آدرسدهیها اونجا میشه شبیه چیزی که داریم میبینیم در الف فایل)، مقدار Entry point address رو تو رجیستر PC میذاره و شروع میکنه ۴ بیت ۴ بیت خوندن و اجرا کردن. یچیزی که الان برام سواله که این فرآیند خوندنه و مچ کردن داخل CPU چطور اتفاق میافته که برا هدفی که الان دارم اهمیتی نداره و اگه یبار دیگه جنگ شد بهش بر میگردم.
نتیجه
تو این پست دیدیم که معماری آرم۶۴ چیه، چطوری اسمبلیش نوشته میشه و اجرا میشه و در نهایت هم تو این روزای ننگ تونستم چند ساعتی خودم رو مشغول کنم و (تا الان) زنده بمونم. خوب بود.