C# va assembler
Bo’sh vaqtlarimda boshqotirma sifatida quyi darajali dasturlash bilan qiziq turganim uchun doim boshqa tillarga assemblerda yozilgan kodlarni tiqishtirib yuraman. Qilmoqchi bo’lgan ishimni to’iq assemblerda yozib qo’ya qolay desam unda boshqotirma emas boshog’riqqa aylanib ketib qolishidan ko’ra, shu usul ma’qulroq.
Aslida .NET platformada CLI uchun o’zini «rodnoy» assembleri bor albatta. Buni IL (Oraliq til) deyiladi. .NET platformadagi ko’p tillar ushbu tilga kompilyasiya qilinadi va JIT kompilyator tomonidan bajariladi. Bunda yozilgan kod .NET dagi yuqori darajali tillardan biroz tezroq ishlaydi. Sababi kompilyasiya jarayonida optimizasiyani dastur odamchalik eplolmasligida albatta. Hullas hozir bizni IL qiziqtirmaydi, maqsad faqat «toza» assembler.
Demak, Windows API tarkibida CallWindowProc degan funksiya bor. Ushbu funksiya oyna «obrabotchigini»* chaqirish uchun ishlatiladi. Ya’ni, odatda oynalarni subclassing qilishda. Sodda qilib aytganda, masalan, biz qandaydir oynani «obrabotchigi»ni o’rniga o’zimizni «obrabotchik»ni qo’yib, kelayotgan kerakli habarlarni «eshitib», unga mos amallarni bajaramiz va bizga keraksiz habarlar shunchaki yo’q bo’lib ketmasligi uchun eski obrabotchikni chaqirib qo’yamiz, «senga quyidagi habar bor» mazmunida. Bunda CallWindowProc ga oyaning eski «obrabotchigi» joylashgan manzilni beramiz (buni oldin aniqlab olgan bo’lamiz albatta). Bunday olib qarasak shu manzilni o’rniga ihtiyoriy kod joylashgan manzilni ham berishimiz mumkin. Ushbu usulda Visual Basic da «toza» kodlarni chaqirishda ishlatardim. Buning uchun oldin kodni biror assemblerda yozib, natijaviy mashina kodlarini biror massivga yozib, CallWindowProc ga ushbu massiv ko’rsatkichini (pointer) uzatilardi.
C# da yoziladigan kodlar CLR tomonidan doimiy nazorat ostidaligini bilamiz (managed code). Pointerlar, xotira bilan to’g’ridan to’g’ri ishlab bo’lmaslik va boshqa cheklovlar ataylab kiritilgan. Dastur xavfsizligini oshirish maqsadida albatta. Biroq Microsoftdagi okalar buyerda emin-erkin (unmanaged) yozish uchun ham har qalay biroz imkon qoldirishgan. C# da unsafe kalit so’zi ostida boshqaruvsiz kodlar qo’yish, fixed blogi ostida pointerlar bilan ishlash mumkin. Biz ham shundan foydalanamiz.
Avvalo C# dan chaqiradigan mashina kodidagi dasturni biror assemblerda yozib olamiz. Man NASM dan foydalanaman. Boshlanishiga faqat 2 ta sonni qo’shadigan kodda tekshirib ko’ramiz.
1.asm fayl yaratamiz (masalan notepad++ da):
%Define param1 [ebp+4] ; param1 %Define param2 [ebp+8] ; param2 %Define param3 [ebp+12] ; param3 %Define param4 [ebp+16] ; param4 [BITS 32] mov ebp, esp mov edi, param1 mov eax, [edi] mov edi, param2 add eax, [edi] mov edi, param3 mov [edi], eax mov esp, ebp ret
Yuqoridagini nasm da kompilyasiya qilamiz:
nasmw -f bin 1.asm -o 1.bin
-f kaliti natijaviy dasturni koddan tashqari ma’lumotlarsiz hosil qiladi. DOS dagi .com kabi, faqatgina kodni o’zi. Natijada 20 baytli 1.bin degan fayl hosil bo’ladi.
Visual Studio ga o’tib C# da konsolli proyekt yaratamiz.
using System; using System.Runtime.InteropServices; namespace ConsoleApplication1 { unsafe class Program { [DllImport("User32.dll")] public static extern IntPtr CallWindowProc(byte* ptr, int* param1 , int* param2 , int* param3 , int* param4 ); static void Main(string[] args) { // mashina kodi byte[] code ={0x89, 0xe5, 0x8b, 0x7d, 0x04, 0x8b, 0x07, 0x8b, 0x7d, 0x08, 0x03, 0x07, 0x8b, 0x7d, 0x0c, 0x89, 0x07, 0x89, 0xec, 0xc3, 0x00}; // code uchun pointerni olish va o'zgaruvchilar uchun // "qo'zg'almas" hududga kirish fixed (byte* pointer = &code[0]) { int p1, p2, p3, p4; p1 = 15; p2 = 75; p3 = p4 = 0; Console.WriteLine("Avval: p1={0} p2={1} p3={2} p4={3}", p1, p2, p3, p4); // kodni chaqirish CallWindowProc(pointer, &p1, &p2, &p3, &p4); Console.WriteLine("Keyin: p1={0} p2={1} p3={2} p4={3}", p1, p2, p3, p4); Console.ReadKey(); } } } }
Class dagi unsafe kalit so’zi nega kerakligini aytdim, unmanaged kod uchun kerak. code nomli massivga natijaviy 1.bin fayli tarkibi yozilgan. Buni qo’lda yozib chiqmaslik uchun boshqa oddiy bir dasturcha qilib oldim, yoki shu dasturni ichida fayldan o’qib olish ham mumkin. pointer nomli o’zgaruvchiga code massivini ko’rsatkichini olamiz va «qo’zg’almas» sohaga kiramiz. Qolgani tushunarli bo’lsa kerak. CallWindowProc funksiyaga kod yozilgan manzilni (pointer) va 4 ta parametrlarni beramiz.
Omadli chipta haqidagi masalani ham shunday ishlatib natijasini solishtirib ko’rdim. Ushbu misolni c# ni o’zidagi yechimidan ~5 marta tezroq hisoblandi. Albatta bu bilan demak assemblerdagi kod C# dagidan faqat 4 marta tez ishlar ekan deb hulosa chiqarish noto’g’ri. Haqiqiy tezliklarini solishtirish uchun hamma kodni assemblerda yozish kerak va boshqa maxsus testlardan foydalanish lozim bo’ladi. Shunday bo’lsada yetarlicha tezroq deyishimiz mumkin. Va ushbu narsa VB da menga ko’p asqotar edi.
Omadli chipta kodi:
byte[] code={0x55, 0x89, 0xe5, 0x81, 0xec, 0x0c, 0x00, 0x00, 0x00, 0x8b, 0x7d, 0x08, 0x8b, 0x07, 0x89, 0x45, 0xf8, 0x8b, 0x7d, 0x10, 0x8b, 0x07, 0x89, 0x45, 0xf4, 0x31, 0xc9, 0x51, 0x8b, 0x7d, 0x0c, 0xc7, 0x07, 0x01, 0x00, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, 0x00, 0x51, 0x89, 0xc8, 0xe8, 0x35, 0x00, 0x00, 0x00, 0x89, 0x5d, 0xfc, 0x89, 0xd9, 0x51, 0x89, 0xc8, 0xe8, 0x28, 0x00, 0x00, 0x00, 0x3b, 0x5d, 0xfc, 0x75, 0x05, 0x8b, 0x7d, 0x0c, 0xff, 0x07, 0x59, 0x81, 0xc1, 0x09, 0x00, 0x00, 0x00, 0x3b, 0x4d, 0xf8, 0x72, 0xe2, 0x59, 0x41, 0x3b, 0x4d, 0xf8, 0x72, 0xce, 0x59, 0x41, 0x3b, 0x4d, 0xf4, 0x72, 0xb8, 0x89, 0xec, 0x5d, 0xc3, 0x31, 0xdb, 0x31, 0xd2, 0xb9, 0x0a, 0x00, 0x00, 0x00, 0xf7, 0xf1, 0x01, 0xd3, 0x85, 0xc0, 0x75, 0xf1, 0xc3, 0x00};
Manba:
Umumiy Dasturlash
C# va assembler