آموزش زبان smali
به نام خدا
اسمالی یک زبان (human-readable) شبیه به اسمبلی هست که برای فایل های اجرایی دالویک(ART-Dalvik) به کار میره این زبان یک لایه حیاطی بین برنامه سطح بالای جاوا و بایت کد های سطح پایین که توسط Dalvik یا ART اجرا میشن هست و در تست نفوذ اپلیکیشن های اندرویدی به شدت کاربرد داره!
دالویک یا ART چیه؟
Dalvik یه ماشین مجازیه که برای اجرا کردن بایت کد های برنامه های اندرویدی استفاده میشه! این ماشین مجازی اختصاصی برای سیستم عامل اندروید طراحی شده تا نرمافزار های جاوایی که با مصرف بهینه منابع، عملکرد کنترل شده و… داخل سیستم اندرویدی اجرا بشن.
باید کد های Dalvik با پسوند .dex به اختصار (Dalvik Executable) داخل اپلیکیشن اندرویدی ذخیره میشن و smali یه زبان رابط بین زبان سطح بالای جاوا و بایت کد های دالویک هست
همچنین بعد تر ART به عنوان نسخه پیشرفته تر از Dalvik در اندروید 4 معرفی شد و در اندروبد 5 به طور کامل جایگزین دالویک شد اما این سیستم در اندروید 7 با ترکیب AOT/JIT راه حلی برای اجرای برنامه ها در سیستم هایی با منابع سخت افزاری کم ارائه کرد.
حالا به درک این رسیدیم که smali دقیقاً چه کاربردی داره و چرا اینقدر مهمه!
قبل از شروع اموزش smali باید یکسری پیشنیاز هایی رو بدونید که همین اول کاری بهش اشاره میکنم تا وسط کار بر نگردین عقب…
اول از همه باید با دالویک و به طور کلی معماری اندروید اشنایی نصبی داشته باشین
دوم باید ساختار یک اپلیکیشن اندرویدی (یه فایل apk) رو بدونین
سوم هم باید با جاوا آشنایی داشته باشین چون این زبان به شدت وابسته به جاوا هستش!
چهارم باید با ابزار هایی مثل apktool و… یه فایل apk رو دیکامپایل کنین و یه چرخی توش بزنین ببینین چه خبره… (برای اینکه با ساختار .apk هم اشنا بشین هم میتونین با winrar یا هر جایگزین دیگه اونو اکسترکت کنین و یه سَرَکی بکشین)
پنجم حالا شما اماده این برای دیدن آموزش smali
اول از همه ببینیم Smali کجای تست نفوذ اپلیکیشن های اندرویدی بکارمون میاد!؟
اسمالی توی اندروید بهتون این اجازه رو میده که عمق کد یک اپلیکیشن دسترسی داشته باشین البته ابزار هایی مثل JADX هستن که فایل .apk رو به جاوا تبدیل میکنن اما شما برای تغییر دادن سورس یه برنامه اندرویدی حتماً به دالویک نیاز پیدا میکنین! درواقع ابزار هایی مثل jd-gui , jadx با استفاده از روش های خودشون این کد های اسمالی رو به جاوا تبدیل میکنن تا خوانایی بهتری داشته باشه! اما گاهی اوقات فقط خوندن مهم نیست…
سرفصل های یادگیری smali :
1 آشنایی با انواع داده ها
2 آشنایی با هدر ها: تعریف کلاس ها توابع annotion ها و…
3 آشنایی با رجیستر ها و انواع اونها: تعریف و شناخت انواع رجیستر
4 آشنایی با فرمان های اجرایی اصلی: دستورات مربوط به حافظه، عملگر های منطقی، فراخوانی متد ها، کار با آبجکت ها و…
5 آشنایی با حلقه ها دستورات شرطی و پیادهسازی آنها
(سعی کردم سرفصل ها طوری باشه که تونگاه اولی که به یه اپلیکیشن اندرویدی داشتین از همون اول متوجه ساختار کلی فایل smali بشین)
که بعضی از سرفصل هارو توی همین مطلب و بقیه رو تو مطالب آینده میگم
تعریف انواع داده در اسمالی:
این مورد رو مثل اغلب زبان های برنامه نویسی داخل یه جدول نشون میدیم و به یه توضیح کوتاه اکتفا میکنیم…
V –> Void C –> Char I –> Integer Z –> Boolean F –> Float D –> Double (64 bit) S –> Short J –> Long (64 bit)
توی این نوع داده ها Void فقط برای ریترن تایپ ها میتونه بکار بره! همچنین از بولین هم به ازای یک نتیجه منطقی و… میشه استفاده کرد
آشنایی با هدر ها:
1- تعریف کلاس ها (و هدر فایل .smali) :
خط اول فایل مثل جاوا شروع به تعریف کلاس مربوط به فایل میکنه درواقع هر فایل محتویات یک کلاس رو داخلش داره ( مثل جاوا ) که اینجا مشخصاتی از اون کلاس تعریف میشه مثل اسمش سطح دسترسی به کلاس و…
با مثال پیش میریم که بهتر متوجه بشین مثلاً خط اول فایل MainActivity.smali از اپلیکیشن سایفون:
.class public Lcom/psiphon3/MainActivity;
با کلمه کلیدی .class تعریف کلاس شروع شده و بعدش پرمیژن کلاس (پابلیک) اومده و بعد ازا اون اسم پکیج و کلاس پشت هم نوشته شده که سینتکس کلیش رو میشه اینطور تعریف کرد:
.class <perm> L<package-name>/<class-name>;
خط دوم :
.super Lcom/psiphon3/psiphonlibrary/u;
سایفون اینجا کلاس والد رو تعریف کرده که با .super و اسم پکیج و کلاس تعریف شدن که میشه سینتکس کلی اون رو هم اینطور توصیف کرد:
.super Lcom/psiphon3/psiphonlibrary/u;
درواقع این خط به کامپیلر میگه که کلاس ما از کلاس u از پکیج سایفون لایبرری مشتق میشه
خط سوم:
.source "SourceFile"
اینجا اسم فایلی که کد های جاوایی داخلش بودن نوشته میشه که معمولاً پسوند .java رو میبینین اما اینجا به هر دلیلی که ممکنه، فقط SourceFile رو نوشته (که ممکنه اسم پوشه یا هر طرفند دیگه ای برای جلوگیری از مهندسی معکوس اون نوشته شده باشه)
بنا براین با .source هم اسم فایل رو مشخص میکنیم…
تعریف توابع در smali:
دامنه تابع با method تعریف میشود که خط اول آن شامل هدر تعریف تابع، اسم، سطح دسترسی، خروجی و… تابع میباشد و در انتهای نیز با [.end method] بدنه تابع تمام میشود
اینبار هم برای درک بیشتر به تعریف تابع کانستراکتور(سازنده) از کلاس MainActivity در نرمافزارسایفون میپردازیم:
# direct methods .method public constructor <init>()V .locals 1 .line 1 invoke-direct {p0}, Lcom/psiphon3/psiphonlibrary/u;→<init>()V … .end method
در خط اول نوع توابعی که بعد تر از آن تعریف میشوند نشان داده شده که میتوان آن را مطابق جدول زیر تعریف کرد که هرکدام مطابق مفاهیم برنامه نویسی سطح بالا به کاربرد های خاصی اشاره دارند.
متدی که به طور مستقیم در کلاس تعریف شده و میتواند به راحتی از طریق یک شیء از آن کلاس فراخوانی شود. |
Direct Method |
سازندهای که برای ایجاد و راهاندازی یک شیء جدید از کلاس استفاده میشود. |
Constructor Method |
متدی که به کلاس تعلق دارد و بدون نیاز به ایجاد یک شیء از آن کلاس میتواند فراخوانی شود. |
Static Method |
متدی که به یک شیء خاص از کلاس تعلق دارد و برای دسترسی به ویژگیها و رفتارهای آن شیء استفاده میشود. |
Instance Method |
متدی که در کلاسهای انتزاعی تعریف شده و باید در کلاسهای فرزند پیادهسازی شود. (در پولیمورفیسم برای کلاس والد استفاده میشود) |
Abstract Method |
متدی که میتواند در کلاسهای فرزند بازنویسی شود و به عنوان یک متد مجازی عمل میکند. (در پولیمورفیسم برای کلاس فرزند استفاده میشود) |
Virtual Method |
بعد از اینکه با تگ .method تعریف تابع اغاز میشود پس از تعیین سطح دسترسی و اسم تابع و تایپ پارامتر های ورودی نیز داخل پرانتز به ترتیب تعریف خواهند شد
همچنین در انتهای همان خط تایپ خروجی تابع نیز مشخص میشود که در این مثال یک متد از نوع پابلیک به اسم کانستراکتور (این اسم بههمراه تگ <init> پس از آن نشان دهنده کلاس کانستراکتور[سازنده] در زبان smali است.) به عنوان سازنده، بدون هیچ پارامتر ورودی و با خروجی void تعریف شده
آشنایی با رجیستر ها در اسمالی (ادامه) :
پارامتر های ورودی تابع نیز داخل پرانتز به ترتیب و تنها با تعیین نوع داده مشخص میشوند سپس در طول برنامه با استفاده از p0 تا pn میتوان به پارامتر های متفاوت دسترسی داشت
همچنین همواره در هر تابعی به متغییر های متفاوتی برای انجام عملیات های منطقی و… نیاز داریم بنا بر این پس از تعریف تابع تعداد متغییر های مورد نیاز رو برای تخصیص حافظه بیان میکنیم (با کلید واژه locals) همچنین برای دسترسی به این متغیر ها هم باید از v0 تا vn استفاده کنیم.
نکته حائز اهمیت در اینجا این است که برای دسترسی به متغیر ها میتوانیم از v0 شروع کنیم اما در پارامتر های ورودی پارامتر p0 برای اشاره گر this رزرو شده و درواقع پارامتر های ورودی ما از p1 شروع خواهند شد.
بهعنوان مثال تگر بخواهیم یک تابع پابلیک با خروجی دابل با ورودی های کاراکتر، استرینگ، بولین و اینتیجر رو تعریف کنیم میتونیم به این شکل اونو بنویسیم
.method public myFunc(CLjava/lang/String;ZI)D
طبق جدول متغیر ها که بالا تر معرفی کردیم حروف داخل پرانتز به ترتیب کاراکتر، استرینگ، بولین و اینتیجر اشاره دارند اما در اینجا ساختار استرینگ کمی متفاوت تر نوشته شده
استرینگ اینجا هم مثل زبان های مثل cpp و… بهعنوان یک نوع داده از پیش تعریف شده وجود نداره اما خود جاوا اونو توی یه کلاس اختصاصی (java/lang/String) اونو تعریف کرده (نمیدونم قبلتر اینو گفتم یا نه) اما باید یه طوری به کامپایلر بفهمونیم که این مسیر یه کلاس هست بنابراین از این به بعد هرجایی که خواستیم از یه کلاس خاصی استفاده کنیم اول اون با L کامپایلر رو متوجه میکنیم که ماداریم به یه کلاس توی این مسیر اشاره میکنیم در انتهای مسیر کلاس هم همونطور که بالاتر نوشتیم با یه سمیکالن میگیم که دیگه مسیرمون تموم شده و کامپایلر زحمت الکی نکشه.
حالا بیاین تا برای یادگیری بیشتر یه تابع addInt بنویسیم که بتونه دوتا اینتیجر رو باهم جمع بزنه توی یه متغیر ذخیره کنه و اونو به خروجی پاس بده:
.method public addInt(II)I .locals 1 add-int v0, p1, p2 return v0 .end method
در اینجا یک متد پابلیک به اسم addInt با دو ورودی اینتیجر تعریف کردیم که یک خروجی اینتیجر هم داشت…
با استفاده از دستورات منطقی اسمالی بهراحتی تونستیم نتیجه جمع دو ورودی p1 و p2 رو توی متغیر v0 ذخیره کنیم و اونو ریترن کنیم!
آشنایی با دستورات کنترلی و منطقی در smali :
اول از همه با دستورات منطقی مثل جمع و تفریق شروع میکنیم که هم ساده تر هستن و هم برای هممون ملموس تر هستن، اینجا توی smali یکم دسترورات ما بیشتر از جمع و تفریقه . دستورات منطقی دیگه مثل XOR هم وجود دارن که بهسادگی پیادهسازی میشن
هرکدام از توابع منطقی به شکل کلی برای نوع داده منحصر به فردی در دسترس است اما بهدلیل ساختار یکپارچه و شبیه به هم از پیچیدگی اضافی پیشگیری شده
همچنین گاهاً برای انجام عملیات های منطقی مثل جمع یا تفریق برای تو عدد از نوع داده های مختلف مثل اینتیجر و دابل، یا اینتیجر و فلوت قبل از انجام عملیات باید نوع داده یکی از متغیر ها تغییر کند.
دستورات مربوط به جمع، تفریق، ضرب و تقسیم به شکل کلی مطابق جدول زیر تعیین میشوند :
جمع دو عدد صحیح |
V0 = p1 + p2 |
add-int v0, p1, p2 |
تفریق دو عدد صحیح |
V0 = p1 - p2 |
sub-int v0, p1, p2 |
ضرب دو عدد صحیح |
V0 = p1 * p2 |
mul-int v0, p1, p2 |
تقسیم دو عدد صحیح |
V0 = p1 / p2 |
div-int v0, p1, p2 |
باقیمانده تقسیم دو عدد صحیح |
V0 = p1 % p2 |
rem-int v0, p1, p2 |
عملگر منطقی AND برای دو عدد صحیح |
V0 = p1 & p2 |
and-int v0, p1, p2 |
عملگر منطقی OR برای دو عدد صحیح |
V0 = p1 | p2 |
or-int v0, p1, p2 |
عملگر منطقی XOR برای دو عدد صحیح |
V0 = p1 ^ p2 |
xor-int v0, p1, p2 |
عملگر منطقی NOT برای عدد صحیح |
V0 = ~p1 |
not-int v0, p1 |
این جدول که انواع عملگر های منطقی و ریاضیاتی در smali برای نوع داده اینتیجر در آن تعبیه شده یک نمونه جامع برای استفاده همین عملگر ها در نوع داده های دیگری مثل Double و… نیز هست که تنها برای اینکار باید بهجای int نوع داده دیگری قرار داده شود
تعریف متغیر با استفاده از رجیستر:
const/4 v2, 20 .local v2, “myVar”:I
در این کد ابتدا مقدار ثابت 20 به رجیستر v2 تخصیص داده میشه اما بعدتر با .local اسم و نوع این متغیر تعیین میشه البته تعیین اسم اینجا فقط جنبه توصیفی برای خوانایی بیشتر رو داره و در ادامه هم از همون v2 یا vn خودمون برای کار با متغیر استفاده میکنم ولی تعیین نوع داده مهمه و توی عملیات های منطقی و… نیاز داریم که نوع دادمون مشخص شده باشه
توجهتون رو به این جلب میکنم که .locals و .local هر دو برای مدیریت متغیر های لوکال داخل متد استفاده میشن، اولی برای مشخص کردن تعداد رجیستری هایی که این متد نیاز داره و دومی هم برای تعیین تایپ داده و اسم متغیر استفاده میشه!
حالا یه سؤال اساسی تر; هر تابع توی یه کلاس تعریف میشه و اون کلاس ممکنه یسری متغیر گلوبال درون خودش داشته باشه که به عنوان ویژگی های اون کلاس شناخته بشن، متد ها اونارو تغییر بدن و مدیریت کنن، این متغیر هارو چطور تعریف میکنیم توی smali ؟
ما به این نوع متغیر ها توی smali فیلد (field) میگیم که خارج از متد ها و توی بدنه تعریف کلاس که بالا تر گفتیم تعریف میکنیم شون توی کلاس نیاز نیست از قبل تعداد فیلد هارو مشخص کنیم و تعریف مقادیرشون هم با .field شروع میشه و نوع کلی تعریفشو میتونیم اینطور نشون بدیم:
.field [modifiers] [name]:[data type]
مثل:
.field public myName:Ljava/lang/String; .field private myNumber:I
حالا یه سری کارای دیگه هم هست که میشه انجام داد مثل تعریف متغیر های استاتیک و ثبات ها که متغیر های استاتیک با کلید واژه static و ثبات هارو با final مشخص میکنیم
.field private final myConstVar:I = 42
فیلد هایی که از نوع ثبات (final) تعریف میشن باید حتماً در زمان تعریف مقدار دهی هم بشن (مثل بالا) اما فیلد های دیگه نمیتونن موقع تعریف مقدار دهی اولیه هم بشن بلکه باید توی توابع سازنده مقداردهی بشن و توابع استاتیک هم توی تابع سازنده استاتیک مخصوص به خودشون مقدار دهی اولیه میشن!
نکته پیشرفته تر:
ما اینجا مودیفایر های بیشتری هم داریم، مثلاً یکی از اونا تنها برای استفاده کامپایلر استفاده میشن که یه جدول این زیر از مودیفایر های مختلف آوردیم:
تعریف دسترسی: دسترسی از همه کلاس ها |
public |
تعریف دسترسی: دسترسی تنها از داخل همان کلاس |
private |
تعریف دسترسی: دسترسی تنها از داخل همان کلاس و کلاس های مشتق شده |
protected |
متغیر های تعریف شده توسط این فیلد به کل کلاس مربوط میشوند |
static |
با این مودیفایر متغیر های ثبات تعریف میشوند |
final |
این مودیفایر برای visibility در مالتی تردینگ در cpu های مدرن استفاده میشود توضیح کامل تر آن پایین تر نوشته خواهد شد |
volatile |
برای جلوگیری از سریالایز شدن مقدار ذخیره شده استفاده میشود در این روش هنگام کش، تبادل بین پراسس ها و… داده های حساس و موقت مثل فایل هندلر ها، پسورد، سشن ها و… نباید منتقل شوند! |
transient |
این مودیفایر برای هماهنگی با برخی زبان های سطح بالاتر به طور خودکار توسط کامپایلر ایجاد میشوند که پرداختن به تئوری این عملکرد خارج از حوصله این مطلب است |
synthetic |
عملکرد volatile : در cpu های مدرن ترد های مختلفی به شکل همزمان کار میکنند و به رجیستر و کش خود cpu دسترسی دارند اما اگر دو ترد به شکل همزمان روی یک متغیر عملیات انجام دهند در نتیجه کدام یک ذخیره خواهد شد!؟ این مودیفایر اطمینان میدهد که متغیر در کش نوشته یا از ان خوانده نشود! همچنین از باز ترتیب بندی دستورات توسط پردازنده هم جلوگیری میکند (البته که شناختن این سه مودیفایر نیازی نیست و فقط برای مطالعه تفریحی جالبن)
حالا میرسیم سراغ اینکه چطور میتونیم به این فیلد ها دسترسی داشته باشیم؟ چطور اونارو بخونیم و بنویسیم؟
میتونیم درحالت کلی فیلد هارو به دو دسته استاتیک و غیر استاتیک تقسیم کنیم و برای هر فیلد از دستورات مربوط به خودشون استفاده کنیم(نحوه کار با فیلد های استاتیک و غیر استاتیک متفاوتن)
با دستور iget میتونیم فیلد های غیر استاتیک (instance)
روند کلی دریافت مقدار یک فیلد از نوع instance اینطورهستش که:
iget [destination], [object], [field]:[field type]
با چند تا مثال بهتر متوجه نحوه عملکردش میشیم:
iget v0, p0, Lcom/example/MyClass;→number:I
درواقع در اینجا فیلد number رو از همین ابجکتی که توش هستیم از داخل کلاس خوندیم (توجه کنین که p0 نشون دهنده همون this هست!)
حالا میخوایم مقدار بولین isValid رو توی آبجکتی که هستیم تغییر بدیم:
const/4 v1, 0x0 iput-boolean v1, p0, Lcom/example/MyClass;→isValid:Z
بنابراین روند ریختن یک مقدار داخل یک فیلد هم به همین ترتیب iget با کلمه کلیدی iput تکرار میشه، توی همین مثال بالا هم متوجه این میشین که داریم مقدار رجیستر v1 رو توی فیلد isValid میریزیم
حالا برای فیلد های استاتیک که نیازی به داشتن آبجکت نداریم! پس میتونیم بدون پاس دادن آبجکت به مقدار فیلد خودمون دسترسی داشته باشیم(البته اینجا بهجای iget, iput از sget, sput استفاده میکنیم که فکر میکنم خودتون متوجه دلیل I و s اول اون شده باشین!)
sget [destination], [field]:[field type]
دقیقاً روند نوشتن توی رجیستری هم به همین شکل هستش:
sput [source], [field]:[field type]
اینجا میتونیم یه محدودیتی رو هم اعمال کنیم، مثلاً با استفاده از iget-boolean میتونیم مقداری که قراره داخل رجیستر ذخیره بشه رو به عنوان یه مقدار خام (raw int) ذخیره نکنه بلکه اونو به عنوان یه مقدار منطقی ذخیره کنه!
بنابراین انواع مختلفی از iget و iput و... هم داریم که برای انواع داده مختلف دیگه بکار میبریم که برای اونم جدول زیر رو میتونیم بنویسیم:
نحوه نوشتن |
نحوه خواندن |
نوع داده |
iput/sput |
iget/sget |
Int, float, boolean, etc |
iput-wide/sput-wide |
iget-wide/sget-wide |
long/double |
iput-object/sput-object |
iget-object/sget-object |
object |
iput-boolean/sput-boolean |
iget-boolean/sget-boolean |
boolean |
iput-byte/sput-byte |
iget-byte/sget-byte |
byte |
iput-char/sput-char |
iget-char/sget-char |
char |
iput-short/sput-short |
iget-short/sget-short |
short |
نحوه فراخوانی متد ها در smali :
اول از همه بذارین تا یادم نرفته یه چیزی رو این وسطا بگم… قبلتر گفته بودم چطور میتونیم به یک کلاس اشاره کنیم اما نگفته بودم که چطور میتونیم به اعضای یک کلاس دسترسی بگیریم!؟
دقیقاً بعد از اشاره کردن به کلاس بعد از یک → میتونیم به عضو (متد، فیلد یا…) اون اشاره کنیم مثلاً برای اشاره کردن به تابع onCreate اینطور مینویسیم که:
Lcom/example/MyClass;→onCreate()V
فقط نکته قابل توجهش اینه که تایپ خروجی رو هم باید مشخص کنین در آخر (اگه تا اینجا اومدین دیگه خودتون متوجه میشین و نیاز به تکرار مکررات من نیست )
به این شیوه ای که اشاره کردیم به متد method signature هم میگن که بعداً باهاش کار داریم
اینجا هم طبق روند مرسوم گذشته یه شیوه کلی از نحوه فراخوانی (invoke) کردن توابع رو میخونیم و بعد به تحلیل فراخوانی انوع توابع میپردازیم…
invoke-[dispatch] [parameters], [method signature]
خب مثل فیلد ها که انواع مختلفی داشتیم برای فراخوانی متد ها هم انواع مختلفی رو داریم که وابسته به اینکه چه نوع متدی داریم از اونا استفاده میکنیم که بازم توی یه جدول تمیز این پایین هست:
کاربرد |
سرعت |
Dispatch |
شیوه فراخوانی |
برای متد های استاتیک استفاده میشه فقط |
خیلی سریعه |
static |
invoke-static |
این برای کانستراکتور ها و متد های پرایویت یه کلاس استفاده میشه |
سریعه |
direct |
invoke-direct |
این برای متد های پابلیک هر کلاسی استفاده میشه |
کنده |
virtual |
invoke-virtual |
این یکی برای متد های اینترفیس ها استفاده میشه دلیل کند بودنشم استفاده itable هست که رسیدن به اون دیگه مورد بحث ما نیست جدا |
خیلی کنده دیگه |
interface |
invoke-interface |
اینجارو، این یکی برای دسترسی به متد های کلاس های والد ما استفاده میشه |
یعک کم سریعه اینم |
super |
invoke-super |
حالا بیاین یکی رو باهم پیادهسازی کنیم مثلا:
invoke-static {}, Lcom/example/util;->getTime()J
اینجا اومدیم و تابع getTime رو صدا زدیم و چون ورودی هم نمیخواست اونجارو خالی گذاشتیم:)
خب بیاین یه کانستراکتور رو از یه کلاس با هم فراخوانی کنیم
new-instance v3, Lcom/example/MyClass; invoke-direct {v3}, Lcom/example/MyClass;-><init>()V
خب این طبیعیه که برای فراخوانی یه کانستراکتور باید یه آبجکت از اون رو هم داشته باشیم تا کانستراکتور اونو صدا بزنیم!(این منطقیه که فقط برای بار اول بعد از ساختن instance از کلاس میتونیم اونو صدا بزنیم)
اینجاهم بعد از اینکه اینستنس خودمونو از کلاس ساختیم اونو به عنوان اولین پارامتر ورودی(توجه کنین این پارامتر ورودیه نهههه خروجی!) براش ارسال میکنیم تا کارشو شروع کنه. (یادتون هست که گفتیم توی همه متد ها p0 همون this هست؟ حالا اینجا باید خودمون هم اینو رعایت کنیم و همیشه اولین پارامتر تابع رو همون اینستنسمون از کلاس رو بدیم مگر اینکه متدمون استاتیک باشه که اصلاً دیگه براش فرقی نداره از چه ابجکتی داره فراخوانی میشه )
حالا یه سوال!
خب اگه تابع ما خروجی داشت کجا میره!؟
اینجاش یکم غیر منطقیه انگار اما باید بعد از فراخوانی با یه سری عملگر های مربوط به حافظه اونو به رجیستر خودمون انتقال بدیم پس بذارین همون مقداری که getTime مون بالاتر برمیگردونه رو اینجا مدیریت کنیم:
invoke-static {}, Lcom/example/util;->getTime()J move-result-wide v5
خب اینجا چرا از wide استفاده کردیم؟ مثل همه جاهای قبلی برای long , double چون با 64 بیت سروکار داریم از این کلیدواژه هم استفاده کردیم اما خب میتونیم هم استفاده نکنیم و بجاش با یه کلاس واقعی کار کنیم:
new-instance v2, Ljava/lang/StringBuilder; invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V const-string v3, “Hello world” invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;→append(Ljava/lang/String;)Ljava/lang/StringBuilder move-result-object v2
انصافاً اینجا دیگه یکم سطح کد رفت بالاتر و خیلی جالب تر شد:)
حالا اگه با move-result خروجی رو به رجیستری انتقال ندیم هم قاعدتاً پاسخ تابع حذف میشه
حالا میریم تا جا به جا کردن داده ها بین رجیستر های مختلف با عملگر حافظه ای move رو هم بررسی کنیم
خب قبل تر با عملگر move-result و انواع اون آشنا شدین اما خود عملگر move برای کپی کردن داده به رجیستر های جدید هم استفاده میشه
بازم طبق روال همیشگی مون یه روند کلی رو برای move معرفی میکنیم:
move [destination] [source]
بازم میتونین حدس بزنین که برای 64 بیتی ها باید از move-wide و برای ابجکت ها هم از move-object استفاده میکنیم
خب خب خب تا اینجا که نباید مشکل سختی وجود داشته باشه مگه اینکه طرز بیان من بد بوده باشه که امیدوارم اینطوری نباشه
اما اینجا قراره بریم سر مباحث مهمتر ( و صد البته کمی سخت تر )
شرط ها در smali:
شرط هارو میتونیم به عنوان یه endpoint مهم توی مهندسی معکوس بدونیم و به عنوان نقاط نهایی تصمیم گیری توسعهدهنده های نرمافزار به اون های نگاه کنیم…
تو smali اینقدر شرط داریم که اگه بخوایم همشونو یاد بگیریم سرمون دود میکشه پس بازم طبق روال معمول یه شیوه کلی تعریف شرط هارو بیان میکنیم و بقیشونو توی یه جدول معرفی میکنیم اما مهمتر از خود if ها else ها هستن! اینجا از دو منظر مهمتره یکی سخت تر بودن مهندسی معکوسشه دومی هم اینه که اگه شرطمون برقرار نبود چی؟ یا اگه شرط های تو در تو داشتیم چی؟ نترسین اینکه مهمه دلیل نمیشه حتماً سخت هم باشه پس نگران نباشین و ادامشم همراهم باشین
توی حلقه ها و شرط ها علیرغم وجود if مثل زبان های سطح بالاتر با یه else ساده و براکت باز و بسته نمیتونیم جریان برنامه رو کنترل کنیم
یسری دستورات مدیریت جریان توی smali داریم که توی شرایط مختلف ازشون استفاده میکنیم، چطور؟ اینطور که مثلا میگیم اگر شرط فلان برقرار بود از این خط بپر به خط بیسان و برنامه رو از اونجا ادامه بده
به اینا دستورات مدیریت جریان برنامه میگیم که انصافاً اونقدرا هم سخت نیستن و احتمالاً ازشون خوشتون میاد چون چیزیه که زبونای سطح بالا ندارن و این یه ایده جالبه :)
نمیتونیم به پردازنده بگیم برو خط فلان چون وقتی برنامه کامپایل میشه دیگه خط فلانی نداریم که بخواد معنی بده در عوض توی اون خط فلانمون یه برچسب میزنیم و به پردازنده میگیم برو سراغ این برچسبه…
لیبل گذاری توی smali خیلی ساده انجام میشه و فقط کافیه با : و اسم لیبلتون اون تیکه رو لیبل گذاری کنین:) فقط بدونین که اسم لیبل باید توی متدی که تعریف میکنین یکتا باشه…
مثلا:
:lable_1
برای رفتن به این لیبل و اون لیبل هم از goto استفاده میکنیم مثلاً
goto :lable_1
خب حالا میریم تا با شرط ها آشنا بشیم :)))
ما یه جدول بلند بالا از انواع شرط ها داریم که خودمم هنوز نتونستم حفظش کنم (چون فکر نمیکنم نیازی باشه برای حفظ کردنش وقت بذارم) و از چیت شیت نگاه میکنم
if vA is equal to vB skip to :cond* |
If-eq vA, vB, :cond* |
If vA is not equal to vB skip to :cond* |
If-ne vA, vB, :cond* |
If vA is less than vB skip to :cond* |
If-lt vA, vB, :cond* |
If vA is less than or equal to vB skip to :cond* |
If-le vA, vB, :cond* |
If vA is greater than vB skip to :cond* |
If-gt vA, vB, :cond* |
If vA is greater than or equal to vB skip to :cond* |
If-ge vA, vB, :cond* |
If vA is equal to zero(0) skip to :cond* |
If-eqz vA, :cond* |
If vA is not equal to zero(0) skip to :cond* |
If-nez vA, :cond* |
If vA is less than zero(0) skip to :cond* |
If-ltz vA, :cond* |
If vA is less than or equal to zero(0) skip to :cond* |
If-lez vA, :cond* |
If vA is greater then zero(0) skip to :cond* |
If-gtz vA, :cond* |
If vA is greater than or equal to zero(0) skip to :cond* |
If-gez vA, :cond* |
خب این دستورای شرطی که نوشتیم اولشون با if شروع میشد که همون شرط خودمونه اما پسوند بعدشم طبق روال smali برای کارای مختلف منطقی تعیین شدن و خیلی هم سعی کردن تو انتخاب اینا سلیقه به خرج بدن اما فعععک نکنم موفق بوده باشن :))))
خب نکته ای که وجود داره اینه که اگر شرط ما درست بود پردازنده به اون لیبلی که جلوش نوشتیم اسکیپ میکنه حالا نکتش کجاست؟ اینکه اگه شرط درست نباشه بدون توجه بهش میره خط بعدی رو میخونه (این یکم با منطق برنامه های سطح بالاتر متفاوته که ممکنه اولش گیج بشین ولی با دو تا مثال کامل دستتون میاد)
راستش این تیکه رو تو یه چیت شیت انگلیسی که دیدم خیلی سرسری رد شده بود و ترفندی زده بود که گیجکننده بود
اینطور توضیح میداد که اولش کد جاوا رو مینوشت و بعدش اسمالی اونو مینوشت بعد برای شرطا این اتفاق افتاده بود:
Java: if (A==B){ //Run code! } ----- Smali: if-ne vA, vB, :cound_0 //Run code! :cound_0
اینجا اومده و کلاً شرط رو برعکس نوشته تا اگه برعکسش درس نبود خط بعدی رو اجرا کنه اما اینو که دیدم واقعاً خودمم گیج شدم که چه کاریه آخه مرد حسابی! (اگه این تیکه رو متوجه نشدین تا آخر این بخش رو بخونین و بعد بیاین ببینین حتماً نظر شمارو هم جلب میکنه)
اینو گفتم که بدونین ممکنه توی شرطها با شیوه های مختلفی مواجه بشین که سردرگم کنن شمارو اما نهایتش این منطق شماست که اگه پی شو بگیرین شمارو به جواب درست میرسونه…
بریم چند مثال بهتر ببینیم هم با لیبل ها بیشتر آشنا بشین هم با کامند if تو smali
من کد جاواییشو با else مینویسم و کد اسمالیش رو هم مینویسم بعد باهم تحلیلش میکنیم (فزض رو هم بر این میگیریم که متغیر ها و رجیستر ها و… از قبل تنظیم شدن و از نوشتن اونا صرف نظر میکنیم)
Java: // 1 if (A==B){ return 1; } else { return 2; } --- // 2 if (A>=B){ return 3; } else { return 4; } --- // 3 if (A<B){ return 5; }
Smali: // 1 if-eq vA, vB, :cond_0 const/4 v0, 2 return v0 goto :end_if_0 :cond_0 const/4 v0, 1 return v0 :end_if_0 --- // 2 if-ge vA, vB, :cond_0 const/4 res, 4 return res goto :end_if_0 :cond_0 const/4 v0, 3 return v0 :end_if_0 --- // 3 if-lt vA, vB, :cond_0 goto :end_if_0 :cond_0 const/4 v0, 5 return v0 :end_if_0
خب بیاین خط به خط کد smali رو باهم بخونیم (تصور کنین هر شرط توی یک تابع جداگانه داره اجرا میشه برای همین لیبل گاهاً باهم یکی هستن اگه اینو توی یه متد بنویسین ارور میده و کامپایل نمیشه…)
با بررسی شرط اول شروع میکنیم:
خط اولش که یه شرط ساده نوشتیم از نوع eq حالا اگر vA == vB برقرار باشه پردازنده جامپ میزنه به لیبل cond_0 و از اون خط به بعد کد اجرا میشه (مقدار 1) رو برمیگردونه…
اگر اون شرطه برقرار نشه میره به خط بعدی و مقدار 2 رو توی رجیستر v0 میریزه و اونو برمیگردونه به خروجی تابع در انتها هم باز جامپ میزنه به لیبل end_if_0 تا دستوراتی که بعد از cond_0 هم نوشته شده اجرا نشه!
شرط دوم:
بازم با خود if شروع میکنیم
اگه if-ge درست باشه میپره به لیبل cond_0 و از اون به بعد رو اجرا میکنه!
و اگر شرط برقرار نباشه هم اونو نادیده میگیره، رجیستر v0 رو با 4 مقدار دهی میکنه و اونو به خروجی تابع پاس میده بعدم با goto به لیبل end_if_0 میپره تا کدای اضافی هم اجرا نشه…
تحلیل شرط سومی هم پای خودتون باشه ببینم چیکار میکنین :)
با همین چیزایی که تا الان یاد گرفتیم میتونین بشینین و تمرینی حلقه لوپ بنویسین برای خودتون تا با عجایب بیشتری آشنا بشین…
خوشحالم که تا اینجای کار رو همراه مون بودین و این سماجت یادگیری رو بهتون تبریک میگم! اگه انتقاد یا نظری داشتین ممنون میشم زیر همین پست برام کامنت کنین همچنین اگر سؤالی داشتین هم بپرسین تلاشمو میکنم جوابگوی همه سؤالاتتون باشم باشم
همچنین اگه جایی از این راهنما نامفهوم بود یا مشکلی توش پیدا کردین هم ممنون میشم توی کامنت ها بگین این مطلب رو هرروز به کمک شما آپدیت میکنم تا در نهایت یه راهنمای جامع و مفید از این زبان توی وب فارسی هم داشته باشیم.