به نام خداوند بخشنده و مهربان!

سلام دوستان عزیز و هموطنان گرامی! اینبار هم با بخش هشتم از دوره آموزشی سیستم عامل در خدمتتون هستیم!

میریم سراغ آموزش s15

خوب دیگه باید بریم سراغ آموزش های مود محافظت شده یا همون مود 32 بیتی البته ما الان باید قسمت دوم بوت لودر رو بنویسیم! یعنی باید بوت لودر فایل KRNLDR رو لود کنه و این فایل هم بیاد و کرنل اصلی رو لود کنه. خوب شاید بگید چه کاریه خوب بوت لودر کرنل رو لود کنه! باید بگم ما میخوایم یه تنظیماتی رو قبل از لود کردن کرنل انجام بدیم که نیاز داریم KRNLDR.SYS رو لود کنیم! مثلا میخوایم با این فایل سیستم عامل رو به مد 32 بیتی ببریم. کاری هایی که میخوایم در مرحله دوم بوت لودر انجام بدیم به شرح زیر هست.

  • فعال کردن و رفتن به مود محافظت شده
  • بازیابی اطلاعات بایوس
  • بارگذاری و اجرای هسته
  • فعال کردن خط آدرس 20 برای دسترسی به 4GB حافظه
  • ارائه اداره کننده وقفه ساده

و غیره. همچنین میتونیم مرحله دوم بوت لودر رو با زبان های سطح بالاتر بنویسیم. معمولا مرحله دوم بوت لودر با زبان C و اسمبلی نوشته می شود!

 

دنیــای 32 بیتــی !

بالاخره به جای باحالش رسیدیم s04  به مود 32 بیتی مودمحافظت شده میگن چون قرار مود 32 بیتی از اطلاعات درون رم محافظت کنه و نزاره هر برنامه اطلاعات برنامه دیگه یا سیستم عامل رو به هم بریزه و خراب کنه اینها به وسیله جدول توصیفگر سراسری (GDT) امکان پذیر خواهد بود! در ادامه خیلی بیشتر درباره GDT صحبت میکنیم! اگه برنامه ای بخواهد به مکانی بره که توسط GDT ممنوع شده باشه یه خطا  General Protection Fault (GPF) تولید خواهد شد.

 

جدول توصیفگر

یک جدول توصیفگر برای ما برخی از چیز ها مثل رم و.. رو توصیف میکنه و به ما میگه کجای حافظه آزاده و کجاش ممنوع هست. سه تا جدول توصیفگر داریم Global Descriptor Table (GDT) و  Local Descriptor Table (LDT) و  Interrupt Descriptor Table (IDT) که آدرس پایه هرکدومش توی یکی از ثبات های GDTR و LDTR و IDTR ذخیره میشه. به خاطر اینکه اینها از ثبات های خاصی استفاده میکنن بنابراین دستور العمل های ویژه و مخصوص خودشون رو هم دارن و اکثر این دستورات اختصاصی معمولا در رینگ 0 یا همون کرنل کار میکنن و اگه برنامه ای بخواهد از این دستورات در رینگ 3 استفاده کنه با خطا مواجه میشه! و از این رو که ما در مود 32 بیتی به جدول وقفه ها دسترسی نداریم دوباره یه خطای دیگه به وجود میاد! حالا میریم سراغ توضیح هریک از جداول نامبرده شده بالا

 

جـــدول توصیفگــر سراســری - Global Descriptor Table

این جدول برای ما خیلی مهمه و شما هم توی بوت لودر و هم توی کرنل خواهید دیدش s11

جدول توصیفگر سراسری GDT نقشه حافظه رو برای ما تعریف میکنه و میگه کجای حافظه قابل اجرا کردن (Code) و کجای حافظه قابل خواندن (Data) هست.

جدول GDT معولا داری سه توصیفگر هست یک توصیفگر پوچ یا Null descriptor که همش با 0 پر میشه یک توصیفگر کد یا Code Descriptor و یک توصیفگر دیتا یا Data Descriptor 

خوبه! حالا باید ببینیم که اصلا توصیفگر چی هست؟ برای GDT هر توصیفگر 8 بایت یا یک QWORD  هست  که مقدار آن خواص توصیفگر رو نشون میده! جدول زیر نشون دهنده مقادیر یک توصیفگر GDT را نشان میدهد!

  • بیت های 56 -63: بیت های 24 تا 32 base address
  • بیت 55 : قطعه قطعه بودن
    • 0: هیچ
    • 1: محدود می شود توسط 4K ضرب
  • بیت 54: نوع سگمت
    • 0: 16 بیت
    • 1: 32 بیت
  • بیت 53: رزو شده - باید صفر باشد
  • بیت های 52: رزو شده برای استفاده سیستم عامل
  • بیت های 48-51: بیت های 16 تا 19 Segment Limit
  • بیت 47 : سگمنت در حافظه است (برای حافظه مجازی استفاده میشود)
  • بیت های 45-46: سطح دسترسی توصیفگر
    • 0: بالاترین (رینگ 0)
    • 3: پایینترین (رینگ 3)
  • بیت 44: بیت توصیفگر
    • 0: توصیفگر سیستمی
    • 1: توصیفگر کد یا دیتا
  • بیت های 41-43: نوع توصیفگر
    • بیت 43: سگمنت قابل اجرا
      • 0: دیتا سگمنت
      • 1: کد سگمنت
    • بیت 42: Expansion direction (Data segments), conforming (Code Segments)
    • بیت 41: قابل خواندن و قابل نوشتن
      • 0: فقط خواندنی (دیتا سگمنت) ، فقط اجرا شدنی (کد سگمنت)
      • 1: خواندنی و نوشتنی (دیتا سگمنت) ، خواندنی و اجرا کردنی (کد سگمنت)
  • بیت 40: بیت دسترسی (برای حافظه مجازی استفاده میشود)
  • بیت های 16-39: بیت های 0-23 Base Address
  • بیت های 0-15: بیت های 0-15  Segment Limit

 

بیاید یه کد ساده از جدول بالا بنویسیم. که توصیفگر کد و دیتا در اون باشد و قابلیت خواندن و نوشتن از اولین آدرس (بایت 0) تا 0xFFFFFFFF را در حافظه داشته باشد. این یعنی این که ما بتونیم هر جای حافظه بخونیم و بنویسیم!

ما برای اولین بار میتونیم به کد زیر یه نگاهی بکنیم!

 ; This is the beginning of the GDT. Because of this, its offset is 0.
 
; null descriptor 
	dd 0 				; null descriptor--just fill 8 bytes with zero
	dd 0 
 
; Notice that each descriptor is exactally 8 bytes in size. THIS IS IMPORTANT.
; Because of this, the code descriptor has offset 0x8.
 
; code descriptor:			; code descriptor. Right after null descriptor
	dw 0FFFFh 			; limit low
	dw 0 				; base low
	db 0 				; base middle
	db 10011010b 			; access
	db 11001111b 			; granularity
	db 0 				; base high
 
; Because each descriptor is 8 bytes in size, the Data descritpor is at offset 0x10 from
; the beginning of the GDT, or 16 (decimal) bytes from start.
 
; data descriptor:			; data descriptor
	dw 0FFFFh 			; limit low (Same as code)
	dw 0 				; base low
	db 0 				; base middle
	db 10010010b 			; access
	db 11001111b 			; granularity
	db 0				; base high

خوبه! این GDT شامل سه توصیفگر هست (هر هشت بایت یک توصیفگر هست). توصیفگر پوچ ،کد و دیتا. هر بیت در توصیفگر های بالا نشان دهنده یک ویژگی است که در جدول بالاتر آورده بودیم! (بالای کد میتونید ببینید)

بزارید کمی جزئی تر به مسئله نگاه کنیم و کمی در این موضوع دقیق بشیم! بنابراین به توضیح هر توصیفگر میریم. توصیفگر پوچ که پیداست همش با صفر پر شده پس به سراغ دوتای دیگه میریم!

یه بار دیگه به کد بالا نگاه میندازیم

; code descriptor:			; code descriptor. Right after null descriptor
	dw 0FFFFh 			; limit low
	dw 0 				; base low
	db 0 				; base middle
	db 10011010b 			; access
	db 11001111b 			; granularity
	db 0 				; base high

ما میتونیم کد بالا رو هم به صورت باینری به شکل زیر در بیاریم!

11111111 11111111 00000000 00000000 00000000 10011010 11001111 00000000

یادتون هست که در جدول بالا بیت های 0 تا 15 (یعنی دو بایت اول) نشان دهنده سگمنت حد (segment limit) بودند. این به این معنیه که ما نمیتونیم از آدرس بزرگتر از 0xffff در یک سگمنت استفاده کنیم (که در دو بایت اول هست) . بنابراین با یک خطا GPF مواجه میشیم!

بیت های 16 تا 39 (سه بایت بعدی) نشون دهنده بیت های 0 تا 23 Base Address هستند (شروع آدرس سگمنت) . در اینجا ما اونو صفر قرار دادیم چون آدرس پایه ما 0 هست و حد (اندازه) اون هم 0xFFFF هست. انتخابگر کد میتونه به هرجای ناحیه 0 تا 0xFFFF حافظه دسترسی داشته باشه!

بایت بعدی (بایت 6) خیلی بایت جالبیه و چیز های جالی توش اتفاق میفته! اجازه بدید بریم تک تک بیت های اونو بررسی کنیم!

db 10011010b 			; access
  • بیت 0 (بیت 40 در GDT) : بیت دسترسی (توسط حافظه مجازی استفاده میشه) - چون ما الان از حافظه مجازی استفاده نمیکنیم فعلا اونو نادیده میگیریم!
  • بیت 1 (بیت 41 در GDT) : بیت قابل خواندن و نوشتن هست که ما اونو ست کردیم بنابراین ما میتونیم بخونیم و اجرا کنیم (سگمنت کد) در ناحیه ی 0 تا 0xFFFF
  • بیت 2(بیت 42 در GDT) : این بیت توسعه جهت دار هست که فعلا کارش نداریم و بعدا درموردش بحث خواهیم کرد
  • بیت 3 (بیت 43 در GDT) : ما با این بیت به پردازنده میگیم که این یک توصیفگر کد یا توصیفگر دیتا هست (اینجا ست کردیم چون توصیفگر کد هست)
  • بیت 4 (بیت 44 در GDT) : این بیت نشان دهنده این هست که توصیفگر ما یک توصیفگر system هست یا یک توصیفگر  code/data . اینجا ما ست کردیم چون توصیفگر کد داریم
  • بیت های 5 و 6 (بیت های 45 و 46 در GDT) : نشون دهنده سطح دسترسی توصیفگر هست (از 0 تا 3) که ما اونو 0 قرار دادیم
  • بیت 7 (بیت 47 در GDT) : برای حافظه مجازیه که آیا سگمنت در حافظه هست یا خیر! ما اونو صفر قراردادیم چون از حافظه مجازی فعلا استفاده نمیکنیم!

بایت دسترسی خیلی بایت مهمی هست! ما به تعریف توصیفگر های مختلف برای ارجای برنامه ها در رینگ 3. زمانی که بخواهیم به بحث کرنل وارد بشیم به این مبحث نگاه دقیق تری خواهیم داشت.

بیاید بایت بعدی رو ببینیم!

db 11001111b 			; granularity
	db 0 			; base high

این بایت رو هم باید بیت به بیت تفسیر کنیم ببینیم چی میگه! s15

  • بیت 0-3 (بیت های 48 تا 51 در GDT) : نشان دهنده ی بیت های 16 تا 19 segment limit هست! 
  • بیت 4 (بیت 52 در GDT) : برای استفاده سیستم عامل محفوظ است
  • بیت 5 (بیت 53 در GDT) : برای برخی از کار های محفوظ است! (شاید در آینده مورد نیاز شود)
  • بیت 6 (بیت 54 در GDT) : نوع سگمنت رو نشون میده! که ما اونو 1 گذاشتیم یعنی 32 بیتی
  • بیت 7 (بیت 55 در GDT) : قطعه قطعه بودن. ما اونو ست کردیم پس هر سگمنت 4 کیلو خواهد بود!

آخریت بایت نشون دهنده بیت های 24 تا 32  base address هست. که البته برابره 0 هست! 

 

توصیفگر دیتا

اگه بخوایم این دو توصیفگر یعنی دیتا و کد رو با هم مقایسه کنیم! خواهیم دید که همه ی بیت ها به جزء یک بیت با هم برابر اون بیت هم بیت 43 هست که اگه به جدول بالا نگاهی دوباره بندازید دلیلش رو خودتون میفهمید. (دلیلش اینه که این بیت نشون دهنده توصیفگر کد و دیتا هست)

 

حالا کار تموم شده و ما نیاز به یک اشاره گر برای جدول توصیفگر سراسری GDT داریم! اشاره گر GDT اندازه جدول و آدرس شروع جدول رو ذخیره میکنه! مثال:»

toc: 
	dw end_of_gdt - gdt_data - 1 	; limit (Size of GDT)
	dd gdt_data 			; base of GDT

gdt_data آدرسشروع GDT هست و end_of_gdt هم یک برچسب انتهای GDT هست! به سایز و قالب این اشاره گر دقت کنید! 

پردازنده از یک رجیستر مخصوص به نام GDTR استفاده میکند که دیتا رو در داخل اشاره گر GDT قرار میده! برای لود کردن GDT درون رجیستر GDTR ما به یک دستورالعمل مخصوص نیاز داریم این دستورالعمل LGDT مخفف Load GDT هست! استفاده از این دستورالعمل بسیار آسونه!

lgdt	[toc]		; load GDT into GDTR

 

جدول توصیفگر محلی -  Local Descriptor Table

جدول توصیفگر محلی کوچکتر از جدول توصیفگر سراسری (GDT) هست! و برای کار های تخصصی استفاده میشه. جدول توصیفگر محلی نقشه حافظه رو تعریف نمیکنه. بعدا بیشتر درباره اش بحث میکنیم! 

 

جدول توصیفگر وقفه

این جدول هم خیلی مهمه. جدول توصیفگر وقفه ، جدول بردار وقفه (IVT) رو تعریف میکنه. جای این توصیفگر در حافظه همیشه از آدرس 0 تا 0x3ff هست. 32 بردار اول توسط سخت افزار رزرو شدن که برای مثال وقفه های General Protection Fault و Double Fault Exception  در آنهاست.

 

آدرس دهی حافظه در مود محافظت شده

به یاد داشته باشید که آدرس دهی مود محافظت شده (مود 32 بیتی) با ادرس دهی مود واقعی (مود 16 بیتی) فرق دارهو مود واقعی برای آدرس دهی از Segment:Offset استفاده میکنه! در صورتی که مود محافظت شده برای آدرس دهی از Descriptor:Offset استفاده میکنه!

این به این معنیه که برای دسترسی به حافه در مود محافظت شده ما نیاز به تصور درستی نسبت به GDT داریم. توصیفگر در CS ذخیره شده این به ما اجازه میده که با توصیفگر درست به حافظه دسترسی داشته باشیم.

برای مثال اگه ما به خواندن از مکانی از حافظه نیاز داشته باشیم

خوب فعلا کافیه! (باید ادامه داده شود)

 

وارد شدن به مود محافظت شده

خوب باید خیلی مواظب باشید که کاری رو اشتباه انجام ندید که ممکن مشکلاتی رو براتون به همراه داشته باشه. وارد شدن به مود 32 بیتی کار نسبتا آسونیه. ما باید یک جدول توصیفگر سراسری با یک واصف دسترسی به حافظه رو لود کنیم و بعد وارد مود محافظت شده بشیم.

مرحله اول : بارگذاری جدول توصیفگر سراسری - GDT

به یاد داشته باشید که جدول GDT به ما میگه چطوری میتونیم به حافظه دسترسی داشته باشیم! اگه ما GDT رو ست نکنیم از GDT پیشفرض استفاده میشه. همانطور که میتوانید تصو کنید این اصلا کار درستی نیست! 

به هرحال، کد زیر یک مرحله اولیه ساده برای اینکار است!

; Offset 0 in GDT: Descriptor code=0
 
gdt_data: 
	dd 0 				; null descriptor
	dd 0 
 
; Offset 0x8 bytes from start of GDT: Descriptor code therfore is 8
 
; gdt code:				; code descriptor
	dw 0FFFFh 			; limit low
	dw 0 				; base low
	db 0 				; base middle
	db 10011010b 			; access
	db 11001111b 			; granularity
	db 0 				; base high
 
; Offset 16 bytes (0x10) from start of GDT. Descriptor code therfore is 0x10.
 
; gdt data:				; data descriptor
	dw 0FFFFh 			; limit low (Same as code)
	dw 0 				; base low
	db 0 				; base middle
	db 10010010b 			; access
	db 11001111b 			; granularity
	db 0				; base high
 
;...Other descriptors begin at offset 0x18. Remember that each descriptor is 8 bytes in size?
; Add other descriptors for Ring 3 applications, stack, whatever here...
 
end_of_gdt:
toc: 
	dw end_of_gdt - gdt_data - 1 	; limit (Size of GDT)
	dd gdt_data 			; base of GDT

توجه داشته باشید که toc اشاره گر به جدول توصیفگر سراسری هست. اولین کلمه در اشاره گر اندازه GDT منهای 1 هست و دابل ورد دوم هم آدرس واقعی GDT این اشاره گر باید از این قالب استفاده کنه. یادتون نره منهای 1 رو بزارید!

ما از یک دستور العمل رینگ 0 استفاده میکنیم! LGDT برای لود کردن GDT در رجیستر GDTR. برای استفاده از این دستورالعمل میتونید مثال زیر رو ببینید!

cli			; make sure to clear interrupts first!
lgdt	[toc]		; load GDT into GDTR
sti

خیلی ساده است! نه؟ حالا باید وارد مود 32 بیتی بشیم!

کد زیر کارهای GDT رو انجام میده و ما اونو توی Gdt.inc که یک فایل ورودی هست رختیم!

;*************************************************
;	Gdt.inc
;		-GDT Routines
;
;	OS Development Series
;*************************************************
 
%ifndef __GDT_INC_67343546FDCC56AAB872_INCLUDED__
%define __GDT_INC_67343546FDCC56AAB872_INCLUDED__
 
bits	16
 
;*******************************************
; InstallGDT()
;	- Install our GDT
;*******************************************
 
InstallGDT:
 
	cli				; clear interrupts
	pusha				; save registers
	lgdt 	[toc]			; load GDT into GDTR
	sti				; enable interrupts
	popa				; restore registers
	ret				; All done!
 
;*******************************************
; Global Descriptor Table (GDT)
;*******************************************
 
gdt_data: 
	dd 0 				; null descriptor
	dd 0 
 
; gdt code:				; code descriptor
	dw 0FFFFh 			; limit low
	dw 0 				; base low
	db 0 				; base middle
	db 10011010b 			; access
	db 11001111b 			; granularity
	db 0 				; base high
 
; gdt data:				; data descriptor
	dw 0FFFFh 			; limit low (Same as code)
	dw 0 				; base low
	db 0 				; base middle
	db 10010010b 			; access
	db 11001111b 			; granularity
	db 0				; base high
 
end_of_gdt:
toc: 
	dw end_of_gdt - gdt_data - 1 	; limit (Size of GDT)
	dd gdt_data 			; base of GDT
 
 
%endif ;__GDT_INC_67343546FDCC56AAB872_INCLUDED__

 

مرحله دوم - وارد شدن به مود محافظت شده

حالا باید بریم سراغ رجیستر CR0 اگه میخواید بدونید اون چیه به جدول پایین نگاهی بیاندازید!

  • بیت 0 (PE) : سیستم را وارد مود 32 بیتی میکند
  • بیت 1 (MP) : انگلیسی Monitor Coprocessor Flag .
  • بیت 2 (EM) : انگلیسی Emulate Flag. When set, coprocessor instructions will generate an exception
  • بیت 3 (TS) : پرچم Task Switched Flag این فلگ زمانی که پردازنده بین تسک ها سویچ میکند ست میشود
  • بیت 4 (ET) : انگلیسی ExtensionType Flag. This tells us what type of coprocessor is installed.
    • 0 - 80287 is installed
    • 1 - 80387 is installed.
  • بیت 5 : استفاده نمیشه.
  • بیت 6 (PG) : فعال کردن Memory Paging

مهمترین بیت همون بیت 0 هست به وسیله ست کردن بیت 0 پردازنده وارد مود 32 بیتی میشه! کد زیر این بیت رو ست میکنه!

mov		eax, cr0			; set bit 0 in CR0-go to pmode
or		eax, 1
mov		cr0, eax

قبل از اینکه وارد مود 32 بیتی بشیم باید وقفه ها رو غیر فعال کنیم! اگه فعال باشه به دلیل اینکه نمیتونیم توی مود 32 بیتی از وقفه ها استفاده کنیم با مشکل مواجه میشه!

به محض اینکه وارد مود 32 بیتی میشیم با مشکل مواجه میشیم! مشکل اینه که ما در مود واقعی از Segment:Offset برای آدرس دهی استفاده میکردیم در صورتی که توی مود حفاظت شده باید از Descriptor:Address آدرس دهی استفاده کنیم!

همچنین مود واقعی نمیدونه GDT چیه در صورتی که در Pmode به اون نیاز داریم! الان CS داره به آخرین سگمتی که اجرا کرده اشاره میکنه نه به توصیفگر!

یادتون هست که ما در مود 32 بیتی CS رو باید روی توصیفگر کد ست میکردیم! الان ما نیاز به انجام این کار داریم!

برای اینکه توصیفگر کد ما در آدرس 0X8 هست (8 باید آفست از شروع GDT) ما باید به اونجا بپریم!

jmp	08h:Stage3		; far jump to fix CS. Remember that the code selector is 0x8!

همچنین ما در مود محافظت شده نیاز به این داریم که تمام سگمنت ها رو ریست کنیم تا به توصیفگر داده اشاره بکنه!

	mov		ax, 0x10		; set data segments to data selector (0x10)
	mov		ds, ax
	mov		ss, ax
	mov		es, ax

یادتون نره که 0X10 همون مکان توصیفگر داده در جدول GDT هست!

کد زیر مرحله دوم بوت لودر هست که امروز اونو نوشتیم!

bits	16
 
; Remember the memory map-- 0x500 through 0x7bff is unused above the BIOS data area.
; We are loaded at 0x500 (0x50:0)
 
org 0x500
 
jmp	main				; go to start
 
;*******************************************************
;	Preprocessor directives
;*******************************************************
 
%include "stdio.inc"			; basic i/o routines
%include "Gdt.inc"			; Gdt routines
 
;*******************************************************
;	Data Section
;*******************************************************
 
LoadingMsg db "Preparing to load operating system...", 0x0D, 0x0A, 0x00
 
;*******************************************************
;	STAGE 2 ENTRY POINT
;
;		-Store BIOS information
;		-Load Kernel
;		-Install GDT; go into protected mode (pmode)
;		-Jump to Stage 3
;*******************************************************
 
main:
 
	;-------------------------------;
	;   Setup segments and stack	;
	;-------------------------------;
 
	cli				; clear interrupts
	xor	ax, ax			; null segments
	mov	ds, ax
	mov	es, ax
	mov	ax, 0x9000		; stack begins at 0x9000-0xffff
	mov	ss, ax
	mov	sp, 0xFFFF
	sti				; enable interrupts
 
	;-------------------------------;
	;   Print loading message	;
	;-------------------------------;
 
	mov	si, LoadingMsg
	call	Puts16
 
	;-------------------------------;
	;   Install our GDT		;
	;-------------------------------;
 
	call	InstallGDT		; install our GDT
 
	;-------------------------------;
	;   Go into pmode		;
	;-------------------------------;
 
	cli				; clear interrupts
	mov	eax, cr0		; set bit 0 in cr0--enter pmode
	or	eax, 1
	mov	cr0, eax
 
	jmp	08h:Stage3		; far jump to fix CS. Remember that the code selector is 0x8!
 
	; Note: Do NOT re-enable interrupts! Doing so will triple fault!
	; We will fix this in Stage 3.
 
;******************************************************
;	ENTRY POINT FOR STAGE 3
;******************************************************
 
bits 32					; Welcome to the 32 bit world!
 
Stage3:
 
	;-------------------------------;
	;   Set registers		;
	;-------------------------------;
 
	mov		ax, 0x10		; set data segments to data selector (0x10)
	mov		ds, ax
	mov		ss, ax
	mov		es, ax
	mov		esp, 90000h		; stack begins from 90000h
 
;*******************************************************
;	Stop execution
;*******************************************************
 
STOP:
 
	cli
	hlt

خوب این قسمت هم خدارو شکر به پایان رسید!

ببینید من این آموزش رو از سایت زیر ترجمه کردم! خیلی چیز هاش رو ننوشتم چون نمیفهمیدم و کم کم باید باهاش آشنا میشدم! کلا شما خودتون برید توی سایت زیر و همه اش رو بخونید قطعا با مطالبی که بنده براتون نوشتم و منبع اصلی میتونید حالا بهتر یاد بگیرید!

امیدوارم مفید بوده باشه!

فعلا، یا علی مدد...!