حالا میخواهیم در برنامه ای که مینویسیم، یک Message Box نمایش دهیم. Prototype تابع آن بصورت زیر است:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
hwnd شماره ای است که به Window تخصیص داده میشود (مشابه File Handle). مقدار این پارامتر برای ما مهم نیست، فقط بخاطر میسپاریم که این شماره برای ما مشخص کننده پنجره ای است که قرار است نمایش داده شود. هرگاه بخواهیم عملیاتی را بر روی پنجره انجام دهیم باید Handle آن را ذکر کنیم.
lpText اشاره گری است به رشته ای که میخواهیم در پنجره نمایش دهیم. در واقع آدرس رشته باید در این پارامتر قرار گیرد.
lpCaption همانطوری که از اسمش پیدا است، Caption پنجره را مشخص میکند.
uType آیکون و تعداد و نوع کلیدهای روی پنجره را ذکر میکند.
حالا برنامه قبلی خود را بصورت زیر تغییر میدهیم:
386.
Model flat, stdcall.
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
data.
MsgBoxCaption DB "First Program", 0
MsgBoxText DB "Win32 Assembly Test", 0
code.
:start
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start
برنامه فوق را اسمبل و لینک کرده و سپس اجرا کنید. پنجره ای براساس تعریف بالا نمایش داده میشود.
یکبار دیگر برنامه را با هم مرور میکنیم. در قسمت Data رشته ها بصورت zString تعریف میشوند، یعنی رشته ای که انتهای آن صفر قرار دارد (رشته ها در ANSI حتماْ باید به این شکل تعریف شوند).
مقادیر ثابت NULL و MB_OK مقادیری هستند که در فایل windows.inc از قبل تعریف شده اند و میتوانیم از آنها استفاده کنیم. اپراتور addr آدرس اپراندش را به یک تابع میفرستد. میتوانید بجای آن از offset نیز استفاده کنید، اگرچه بین این دو تفاوتهایی وجود دارد:
addr نمیتواند آدرس مقادیری که بعداْ تعریف میشوند را برگرداند ولی offset میتواند این کار را انجام دهد. به مثال زیر توجه کنید:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
........
MsgBoxCaption DB "First Program", 0
MsgBoxText DB "Win32 Assembly Test", 0
در این وضعیت اسمبلر خطا برمیگرداند، چرا که متغیر بعد از استفاده از addr تعریف شده است. ولی در همین مثال میتوان بدون هیچ مشکلی از offset استفاده کرد.
تفاوت دیگر در این است که offset برای متغیرهای محلی تعریف نشده است، در حالی که addr براحتی با آنها کار میکند (متغیرهای محلی فضاهایی رزرو شده در Stack هستند که آدرس آنها فقط در زمان اجرا مشخص میشود و از آنجایی که Offset در زمان اسمبل کردن ترجمه میشود، طبیعی است که نمیتوان از آن برای متغیرهای محلی استفاده نمود).
تا بعد!!
برگردیم به ExitProcess که در آن پارامتر uExitCode مقداری است که پس از اتمام برنامه و بازگشت، به ویندوز برمیگردد. میتوانید این تابع را بصورت زیر فراخوانی نمایید:
invoke ExitProcess, 0
اگر این عبارت را بلافاصله پس از start قرار دهید، برنامه بلافاصله به ویندوز باز میگردد. به مثال زیر که اولین برنامه ما میباشد، توجه کنید:
386.
model flat, stdcall.
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
data.
code.
:start
invoke ExitProcess, 0
end start
option casemap:none به MASM میگوید که مابین حروف کوچک و بزرگ تفاوت قائل شود (Case Sensitive). بنابراین ExitProcess با exitprocess تفاوت دارد.
include هم یکی از Directive ها است که بمنظور اضافه کردن فایلهای inc بکار میرود. در مثال فوق ما یکی از توابع kernel32.dll را فراخوانی میکنیم، بنابراین لازم است که prototype آن را که در فایل kernel32.inc است را در برنامه داشته باشیم، که این کار توسط include انجام میشود. در صورتی که در برنامه kernel32.inc را ذکر نکنیم، مجاز هستیم از ExitProcess بصورت ساده (بدون invoke) استفاده کنیم. بصورت خلاصه اینکه تنها زمانی میتوان یک تابع را invoke نمود که برنامه prototype آنرا بشناسد.
includelib یکی دیگر از Directiveهایی است که به اسمبلر میگوید برنامه از چه Library هایی استفاده میکند. زمانی که اسمبلر به این Directive برسد، در فایل Object یک دستور Linker قرار میدهد و در زمان لینک، لینکر Library مربوطه را به فایل Object اضافه میکند. با وجود اینکه میتوان Libraryهای مورد نیاز برنامه را در هنگام لینک کردن با دستور Link در خط فرمان ذکر کرد، ولی استفاده از includelib در برنامه روش مطمئنتری میباشد.
برنامه فوق را با نام msgbox.asm ذخیره کرده و آنرا بصورت زیر اسمبل میکنیم:
ml /c /coff /Cp msgbox.asm
c/ به MASM میگوید که فقط عمل اسمبل کردن را انجام دهد ولی برنامه را لینک نکند.
coff/ به MASM یادآوری میکند که فایل object را با فرمت Common Object File Format یا COFF بسازد.
Cp/ به MASM گوشزد میکند که شناسه های تعریف شده توسط برنامه نویس را بصورت Case Sensitive در نظر بگیرد (در واقع میتوان این کار را با اضافه کردن عبارت option casemap:none بلافاصله پس از Model نیز انجام داد).
پس از اسمبل کردن برنامه با موفقیت، همانطوری که میدانید فایل msgbox.obj ساخته میشود و این اولین گام است. حال لازم است که برنامه را لینک کنیم:
link /SUBSYSTEM:WINDOWS /LIBPATH:C:\MASM32\LIB msgbox.obj
SUBSYTEM/ مشخص کننده نوع EXE فایلی است که ساخته میشود.
LIBPATH/ هم مکان فایلهای کتابخانه ای را مشخص میکند که معمولاْ MASM32\LIB است.
پس از لینک کردن فایل اجرایی msgbox.exe که تحت ویندوز قابل اجرا است، ساخته میشود. این فایل را میتوانید اجرا کنید، ولی همانطوری که دیدیم این برنامه کار خاصی را انجام نمیدهد. در درس بعد برنامه ای مینویسیم که کار خاصی را انجام دهد!
تا بعد!!
386.
model flat, stdcall.
data.
code.
:start
.....
end start
همانطوری که میدانید اجرای برنامه از محلی شروع میشود که نام مقابل end انتهای برنامه مشخص میکند. یعنی در برنامه فوق برنامه از دستور بعد از :start شروع میشود. زمانی که دستورات برنامه به پایان میرسد و نیاز است که کنترل به ویندوز بازگردد، یک API فراخوانده میشود: ExitProcess.
ExitProcess Proto uExitCode:DWORD
خط فوق فراخوانی یک Prototype تابع است. Protype تابع ویژگیهای یک تابع را برای اسمبلر/لینکر تعریف میکند و اسمبلر/لینکر از طریق این تعریف میتواند نوع پارامترهای تابع را از نظر صحیح بودن بررسی کند. قالب کلی یک Prototype تابع بصورت زیر است:
...,FunctionName PROTO [ParameterName]:DataType, [ParameterName]:DataType
یا بصورت خلاصه، نام تابع با کلمه کلیدی Proto و سپس لیستی از پارامترهای ارسالی و نوع آنها (که با کاما از هم تفکیک میشوند) در این تعریف نوشته میشود. در مثالی که برای ExitProcess ذکر شد، میبینیم که فقط یک پارامتر وجود دارد و نوع آن Double Word است. Prototypeها زمانی که از invoke (فراخوانی از نوع High-Level) استفاده میکنید، بسیار مفید است. invoke یک فراخوانی ساده است که عمل بررسی نوع پارامترها را هم انجام میدهد.
بعنوان مثال اگر از این دستور استفاده کنید:
call ExitProcess
بدون اینکه قبل از آن یک Double Word را در Stack قرار داده باشیم، پس از اتمام برنامه اسمبلر/لینکر نمیتواند نوع خطایی که احتمالاْ در برنامه پیش آمده را به برنامه نویس برگرداند. ولی اگر از invoke استفاده کنیم:
invoke ExitProcess
لینکر به شما پیغام خطا میدهد که فراموش کرده اید یک Double Word در Stack قرار دهید. قالب کلی invoke بشکل زیر است:
[INVOKE expression [,arguments
expression میتواند نام یک تابع یا اشاره گری به آن باشد. پارامترهای تابع با کاما از هم جدا میشوند.
تقریباْ تمامی توابع API در فایلهای Include نگهداری میشوند. اگر از MASM32 استفاده کنیم، فولدری به همین نام در هنگام نصب برنامه ایجاد میشود (MASM\INCLUDE) که شامل فایلهای Include است. پسوند این فایلها همانطوری که میدانید inc. بوده و protoype توابع موجود در DLLها در آنها ذخیره شده است. این فایلها همنام DLLها هستند ولی با پسوند inc. بعنوان مثال ExitProcess متعلق به Kernel32 است و بهمین علت protoype آن در فایل kernel32.inc قرار دارد. شما هم میتوانید برای توابع خود prototype بسازید و آنرا در فایل Include که برای خودتان ایجاد کرده اید، قرار دهید.
تا بعد!!
زمانی که یک برنامه تحت ویندوز در حافظه Load میشود، ویندوز اطلاعات ذخیره شده در برنامه را میخواند. این اطلاعات شامل نام توابعی که در برنامه مورد استفاده قرار گرفته و DLL هایی که این توابع در آنها قرار دارند، میباشد. زمانی که ویندوز اطلاعات مورد نظر را در برنامه میبیند، DLL مربوطه را در حافظه Load کرده و آدرس تابع استفاده شده در برنامه را با آدرس واقعی حافظه تغییر میدهد.
بصورت کلی دو دسته توابع API وجود دارند: ANSI و Unicode. اسامی توابع ANSI با حرف A خاتمه می یابند، مانند: MessageBoxA. همچنین توابع Unicode با حرف W نشان داده میشوند. بصورت استاندارد ویندوز ۹۵ از توابع ANSI و ویندوز NT از توابع Unicode استفاده میکنند.
ما با رشته های ANSI آشنا هستیم، یعنی آرایه ای از کاراکترها که با کاراکتر Null خاتمه یافته است. طول هر کاراکتر ANSI یک بایت است. با وجود اینکه استاندارد ANSI قابلیت نمایش حروف زبانهای اروپایی را دارد ولی قادر نیست زبانهای شرقی را که شامل تعداد زیادی کاراکتر خاص هستند، حمایت کند. به همین علت Unicode بوجود آمد. هر کاراکتر در Unicode دو بایتی است و بنابراین ۶۵۵۳۵ نوع کاراکتر مختلف را میتواند در یک رشته نمایش دهد.
در اکثر اوقات شما نام API مورد نیاز خود را در برنامه ذکر میکنید و نوع Platform بصورت اتوماتیک بنا بر وضعیت موجود، انتخاب میشود.
این نکات را گفتیم، چون در برنامه هایی که مینویسیم، از این نوع توابع استفاده میکنیم.
تا بعد!!
?DATA.
CONST.
CODE.
هر چهار Directive فوق مشخص کننده بخشهای مختلف برنامه می باشند (دقت کنید قبلاْ به این قسمتها سگمنت میگفتیم ولی اینجا دیگر سگمنت وجود ندارد). در win32 میتوان تمام فضاهای آدرسی را به بخشهایی تقسیم بندی کرد، که شروع هر بخش، انتهای بخش قبل را مشخص میکند. این بخشها در دو گروه دسته بندی میشوند: Data و Code. بخش اول (یعنی Data) سه نوع طبقه بندی دارد:
Data.: در این قسمت داده هایی تعریف میشوند که دارای مقدار اولیه باشند.
?Data. : در این قسمت داده هایی تعریف میشوند که بعداْ مقداردهی میگردند. گاهی اوقات پیش می آید که بخواهیم فضایی را برای یک متغیر در حافظه در نظر بگیریم، ولی به آن متغیر مقدار ندهیم. مزیت این کار این است که حجم برنامه در مقایسه با متغیری که از قبل دارای مقدار باشد، کمتر میشود. یعنی در زمان اجرا اینگونه متغیرها از حافظه فضا میگیرند.
Const. : همانطوری که از اسمش پیداست در این بخش مقادیر ثابت تعریف میشوند.
نکته قابل توجه این است که لزوماْ تمام بخشهای فوق نباید در یک برنامه وجود داشته باشد، هر قسمتی که نیاز داشتید در برنامه اضافه کنید.
برای Code هم فقط یک بخش وجود دارد که با عبارت Code. آغاز میشود. دستورات برنامه باید مابین
<end <label
قرار گیرند که Label یک نام اختیاری است.
تا بعد!!
این یکی از Directive هاست که به اسمبلر یادآوری می کند از مجموعه دستورالعملهای ریزپردازنده ۳۸۶ استفاده کند. همچنین میتوان 486. یا 586. را نیز بکار برد، ولی فعلاْ ما همین 386. را در برنامه ها مینویسیم. البته برای هر مدل پردازنده از دو فرم مشابه میتوان استفاده کرد: 386P یا 386 و یا 486P یا 486. مدل P تنها زمانی بکار میرود که در برنامه ها بخواهیم از دستورات Privileged استفاده کنیم (این دستورات در حالت Protected ، دستورات رزرو شده توسط CPU و سیستم عامل هستند. نمونه برنامه هایی که از این دستورات در آنها وجود دارد Virtual Device Driver ها هستند). در اکثر اوقات برنامه هایی که ما مینویسیم بصورت Non-Privileged هستند و بنابراین از Directive های ساده مثل 386. استفاده میکنیم.
Model Flat, StdCall.:
همانطوری که میدانید این Directive مدل حافظه ای برنامه را مشخص میکند و همانطوری که قبلاْ گفتیم در Win32 تنها مدلی که استفاده میشود مدل Flat است.
StdCall به اسمبلر روش ارسال پارامتر را توضیح میدهد. این روش می تواند چپ براست یا راست به چپ باشد، همچنین چگونگی تنظیمات Stack بعد از فراخوانی توابع توسط این Directive تعیین می شود.
در win16 دو قاعده برای فراخوانی وجود دارد: C و Pascal.
در روش C پارامترها از راست به چپ ارسال می شوند، یعنی سمت راست ترین پارامتر، اول Push میشود. در این حالت برنامه فراخواننده مسئولیت تنظیم قالب Stack را در هنگام فراخوانی برعهده دارد. بعنوان مثال زمانی که تابع زیر به روش C فراخوانده میشود:
(foo(int first_param, int second_param, int third_param
کد اسمبلی بصورت زیر خواهد بود:
push [third_param] ; Push the third parameter
push [second_param] ; Followed by the second
push [first_param] ; And the first
call foo
add sp, 12 ; The caller balances the stack frame
در روش Pascal مراحل برعکس روش قبل می باشد. یعنی پارامترها از چپ براست ارسال میشوند و بنابراین برنامه فراخوانده شده مسئولیت تنظیم قالب Stack را برعهده دارد.
روش C زمانی بکار میرود که در هربار فراخوانی تعداد متفاوتی پارامتر ارسال میشود. نمونه این مسئله را در فراخوانی تابع ()wsprintf دیده میشود. در هنگام فراخوانی این تابع چون خود تابع نمیداند که در هنگام فراخوانی چند پارامتر ارسال میشود در نتیجه نمیتواند قالب Stack را تنظیم کند.
StdCall تلفیقی از روشهای C و Pascal میباشد. یعنی پارامترها از راست به چپ ارسال میشوند ولی برنامه فراخوانده شده وظیفه تنظیم قالب Stack را برعهده دارد.
Win32 بصورت کاملاْ انحصاری از این روش پیروی میکند.
تا بعد!!
در حالت ۳۲ بیتی دیگر نیازی به سگمنت و مدلهای مختلف حافظه نداریم، چرا که در این حالت فقط یک مدل حافظه ای (Flat) وجود دارد و دیگر از سگمنتهای ۶۴ کیلوبایتی خبری نیست. فضای حافظه به صورت یکپارچه و به اندازه ۴ گیگابایت در دسترس است. همچنین دیگر نیازی به ثباتهای سگمنت دیده نمیشود. می توان از هریک از ثباتهای سگمنت برای آدرسدهی به هرجای حافظه استفاده کرد و این کمک بسیار بزرگی به برنامه نویسی است که قبلاْ با زبانهای سطح بالا براحتی کار میکرده است.
زمانی که تحت ویندوز ۳۲ بیتی برنامه نویسی می کنید، باید چند قانون مهم را بدانید. اولین قانون این است که ویندوز از ثباتهای ESI و EDI و EBP و EBX برای عملیات داخلی خود استفاده میکند. در صورتی که از هر یک از این چهار ثبات در برنامه استفاده شود، باید مقادیر اولیه در جایی ذخیره شده و در انتها به ثباتها بازگردند.
ساختار یک برنامه اسمبلی تحت ویندوز بشکل زیر است:
386 .
.MODEL Flat, STDCALL
DATA .
......
?DATA.
......
CONST .
......
CODE .
همین. حالا باید هر بخش را با هم مرور کنیم، که در درس های بعد این کار را انجام میدهیم.
تا بعد!!
شاید کسی حوصله پیگیری نداشته باشه، ولی مینویسم برای دل خودم، شاید یکی پیدا شد و دنبال کرد این خضعبلاتو!!