Win32汇编:过程与宏调用

在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种后进先出(LIFO,Last-In,First-Out)的数据结构,这是因为最后压入堆栈的值总是最先被取出,而新数值在执行PUSH压栈时总是被加到堆栈的最顶端,数据也总是从堆栈的最顶端被取出,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场.

堆栈操作指令

在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种后进先出(LIFO,Last-In,First-Out)的数据结构,这是因为最后压入堆栈的值总是最先被取出,而新数值在执行PUSH压栈时总是被加到堆栈的最顶端,数据也总是从堆栈的最顶端被取出,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场.

当程序运行时,栈是由CPU直接管理线性内存数组,它使用两个寄存器(SS和ESP)来保存堆栈的状态.在保护模式下,SS寄存器存放段选择符(Segment Selector)运行在保护模式下的程序不能对其进行修改,而ESP寄存器的值通常是指向特定位置的一个32位偏移值,我们很少需要直接操作ESP寄存器,相反的ESP寄存器总是由CALL,RET,PUSH,POP等这类指令间接性的修改.

接着来简单介绍下关于堆栈操作的两个寄存器,CPU系统提供了两个特殊的寄存器用于标识位于系统栈顶端的栈帧.
ESP 栈指针寄存器: 栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶.
EBP 基址指针寄存器: 基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部.

◆堆栈参数传递◆

在通常情况下ESP是可变的,随着栈的生产而逐渐变小,而EBP寄存器是固定的,只有当函数的调用后,发生入栈操作而改变.

1.在32位系统中,执行PUSH压栈时,堆栈指针自动减4,再将压栈的值复制到堆栈指针所指向的内存地址.
2.在32位系统中,执行POP出栈时,从栈顶移走一个值并将其复制给内存或寄存器,然后再将堆栈指针自动加4.
3.在32位系统中,执行CALL调用时,CPU会用堆栈保存当前被调用过程的返回地址,直到遇到RET指令再将其弹出.

PUSH/POP指令: 在32位环境下,分别将数组中的元素100h-300h压入堆栈,并且通过POP将元素反弹出来.

.data
	Array DWORD 100h,200h,300h,400h
.code
	main PROC
		xor eax,eax
		push eax                      ; push 0
		push DWORD PTR [Array]        ; push 100
		push DWORD PTR [Array+4]      ; push 200
		push DWORD PTR [Array+8]      ; push 300
		pop eax                       ; pop 300
		pop eax                       ; pop 200
		pop eax                       ; pop 100
		pop eax                       ; pop 0

		push 0
		call ExitProcess
	main ENDP
END main

PUSHFD/POPFD指令: PUSHFD在堆栈上压入EFLAGS寄存器的值,POPFD将堆栈的值弹出并送至EFLAGS寄存器.

.data
	SaveFlage DWORD ?
.code
	main PROC
		pushfd            ; 标识入栈
		pop SaveFlage     ; 弹出并保存到内存

		push SaveFlage    ; 从内存取出,并入栈
		popfd             ; 恢复到EFLAGS寄存器中

		push 0
		call ExitProcess
	main ENDP
END main

PUSHAD/POPAD指令: 将通用寄存器按照EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI的顺序压栈保存.

.code
	main PROC
		pushad
		mov eax,1000
		mov ebx,2000
		mov ecx,3000
		mov edx,4000
		popad

		push 0
		call ExitProcess
	main ENDP
END main

◆声明局部变量◆

高级语言程序中,在单个过程中创建使用和销毁的变量我们称它为局部变量(local variable),局部变量是在程序运行时,由系统动态的在栈上开辟的,在内存中通常在基址指针(EBP)之下,尽管在汇编时不能给定默认值,但可以在运行时初始化,如下一段伪代码:

void MySub()
{
	int var1 = 10;
	int var2 = 20;
}

上面的一段代码经过C编译器转换后,会变成如下的样子,其中EBP-4必须是4的倍数,因为默认就是4字节存储.

MySub PROC
	push ebp                  ; 将EBP存储在栈中
	mov ebp,esp               ; 堆栈框架的基址
	sub esp,8                 ; 创建局部变量空间

	mov DWORD PTR [ebp-4],10  ; var1 = 10
	mov DWORD PTR [ebp-8],20  ; var2 = 20

	mov esp,ebp               ; 从堆栈上删除局部变量
	pop ebp                   ; 恢复EBP指针
	ret 8                     ; 返回,清理堆栈
MySub ENDP

如果去掉了上面的mov esp,ebp,那么当执行pop ebp时将会得到EBP等于10,执行RET指令会导致控制转移到内存地址10处执行,从而程序会崩溃.

为了使代码更加的容易阅读,可以在上面的代码的基础上给每个变量的引用地址都定义一个符号并在代码中使用这些符号来完成编写.

var1_local EQU DWORD PTR [ebp-4]
var2_local EQU DWORD PTR [ebp-8]

MySub PROC
	push ebp
	mov ebp,esp
	sub esp,8
	mov var1_local,10
	mov var2_local,20
	mov esp,ebp
	pop ebp
	ret 8
MySub ENDP

◆ENTER/LEAVE 伪指令◆

ENTRE指令自动为被调用过程创建堆栈框架,它为局部变量保留堆栈空间并在堆栈上保存EBP,该指令执行后会执行以下动作.

1.在堆栈上压入EBP(push ebp)
2.把EBP设为堆栈框架的基指针(mov ebp,esp)
3.为局部变量保留适当的空间(sub esp,numbytes)

ENTER指令有两个参数,第一个操作数是一个常量,用于指定要为局部变量保留多少堆栈空间(numbytes),第二个参数指定过程的嵌套层数,这两个操作数都是立即数,numbytes总是向上取整为4的倍数,以使ESP按照双字边界地址对其.

比如以下代码,使用ENTER为局部变量保存8字节的堆栈空间:

MySub PROC
	enter 8,0
MySub ENDP

经过编译器转换后,会首先转换为以下的样子:

MySub PROC
	push ebp
	mov ebp,esp
	sub esp,8
MySub ENDP

上面的代码只有开头没有结尾,如果要使用ENTER指令分配空间的话,则必须在结尾加上LEAVE指令,这样程序才完整.

MySub PROC
	enter 8,0
....
	leave
	ret
MySub ENDP

下面代码和上面代码作用是相同的,它首先为局部变量保留8字节的堆栈空间然后丢弃.

MySub PROC
	push ebp
	mov ebp,esp
	sub esp,8
....
	mov esp,ebp
	pop ebp
	ret
MySub ENDP

◆USES/LOCAL 伪指令◆

USES操作符: 该操作符用于指定需要压栈的寄存器,其会自动生成压栈出栈代码无需手动添加.

.code
	main PROC
		mov eax,1
		mov ebx,2
		mov ecx,3
		call mycall
		push 0
		call ExitProcess
	main ENDP

	mycall PROC USES eax ebx ecx     ; 生成压栈代码,自动压eax,ebx,ecx
		xor eax,eax              ; 压栈的寄存器可以随意修改
		xor ebx,ebx              ; 过程结束后会自动恢复这些寄存器
		ret
	mycall ENDP
END main

LOCAL操作符: 在过程内声明一个或多个命名局部变量,并赋予相应的尺寸属性,该语句必须紧跟PROC指令后面.

.code
	main PROC
		LOCAL var1:WORD
		LOCAL var2:DWORD,var3:BYTE

		mov DWORD PTR [var1],1024
		mov eax,DWORD PTR [var1]
		mov [var2],1024            ; DWORD
		mov eax,[var2]
		mov [var3],10              ; BYTE
		mov al,[var3]
		push 0
		call ExitProcess
	main ENDP
END main

局部变量:

.code
	lyshark PROC var1:WORD,var2:DWORD
		LOCAL @loca1:BYTE,@loca2:DWORD
		LOCAL @local_byte[100]:BYTE
		
		mov ax,var1
		mov ebx,@loca2
		
		lea ecx,@local_byte
		mov @local_byte[0],0
		mov @local_byte[1],1
		mov @local_byte[2],2
		mov @local_byte[3],3
	lyshark ENDP

	main PROC
		invoke lyshark,100,10000
		ret
	main ENDP
END main

LOCAL(申请数组):

.code
	main PROC
		LOCAL var[3]:DWORD
		
		mov var[0],100
		mov var[1],200
		
		mov eax,var[0]
		mov ebx,var[1]
	main ENDP
END main
.code
	main PROC
		LOCAL ArrayDW[10]:DWORD
		LOCAL ArrayB[10]:BYTE

		lea eax,[ArrayDW]
		mov [ArrayDW],10
		mov [ArrayDW + 4],20
		mov [ArrayDW + 8],30
	main ENDP
END main

过程调用指令

CALL指令指示处理器在新的内存地址执行指令,当用户调用CALL指令时,该指令会首先将CALL指令的下一条指令的内存地址压入堆栈保存,然后将EIP寄存器修改为CALL指令的调用处,等调用结束后返回从堆栈弹出CALL的下一条指令地址.

1.当遇到CALL指令时,程序会经过计算得到CALL指令的下一条指令的地址,并将其压入堆栈.
2.接着会将EIP寄存器的地址指向被调用过程的地址,被调用过程被执行.
3.最后过程内部通过RET指令返回,将从堆栈中弹出EIP的地址,程序继续向下执行.
4.CALL相当于push+jmp,RET相当于pop+jmp.

普通参数传递:

.code
	sum PROC var1:DWORD,var2:DWORD,var3:DWORD
		mov eax,var1
		mov ebx,var2
		mov ecx,var3
		ret
	sum ENDP

	main PROC
		invoke sum,10,20,30      ; 调用并传递参数
		ret
	main ENDP
END main

寄存器传递参数:

.code
	sum PROC
		add eax,ebx
		add eax,ecx
		ret
	sum ENDP

	main PROC
		mov eax,10
		mov ebx,20
		mov ecx,30
		call sum
		ret
	main ENDP
END main

使用PROTO声明: 如果调用的函数在之后实现, 须用 PROTO 提前声明,否则会报错

sum PROTO :DWORD,:DWORD,:DWORD ; 函数声明的主要是参数类型,省略参数名

.code
	main PROC
		invoke sum,10,20,30    ; 现在调用的是之后的函数
		ret
	main ENDP

	sum PROC var1,var2,var3
		mov eax,var1
		add eax,var2
		add eax,var3
		ret
	sum ENDP
END main

CALL/RET指令: 编写一个过程,实现对整数数组的求和,并将结果保存到EAX寄存器中.

.data
	array DWORD 1000h,2000h,3000h,4000h,5000h
	theSum DWORD ?
.code
	main PROC
		mov esi,offset array          ; ESI指向array
		mov ecx,lengthof array        ; ECX=array元素个数
		call ArraySum                 ; 调用求和指令
		mov theSum,eax                ; 将结果保存到内存
		push 0
		call ExitProcess
	main ENDP

	ArraySum PROC
		push esi           ; 保存ESI,ECX
		push ecx
		mov eax,0          ; 初始化累加寄存器
	L1:
		add eax,[esi]      ; 每个整数都和EAX中的和相加
		add esi,TYPE DWORD ; 递增指针,继续遍历
		loop L1
		pop ecx            ; 恢复寄存器
		pop esi
		ret
	ArraySum ENDP
END main

通过该语句块配合可以生成自定义过程,下面我们创建一个名为Sum的过程,实现EBX+ECX并将结果保存在EAX寄存器中.

.data
	TheSum DWORD ?
.code
	main PROC
		mov ebx,100     ; 传递ebx
		mov ecx,100     ; 传递ecx
		call Sum        ; 调用过程
		mov TheSum,eax  ; 保存结果到TheSum

		push 0
		call ExitProcess
	main ENDP
	
	Sum PROC
		xor eax,eax
		add eax,ebx
		add eax,ecx
		ret
	Sum ENDP
END main

INVOKE调用系统API: 默认情况下,会将返回结果保存在eax寄存器中.

.data
	szCaption db "MsgBox",0
	szText db "这是一个提示框,请点击确定完成交互!",0
.code
	main PROC
		.WHILE (1)
			invoke MessageBox,NULL,offset szText,offset szCaption,MB_YESNO
			.break .if(eax == IDYES)
		.ENDW
		ret
	main ENDP
END main

模块化调用: 首先创建一个sum.asm然后在main.asm中引用sum这个文件中的函数.

; sum.asm 首先编译这个文件,并将其放入指定目录下
	.386
	.model flat, stdcall
.code
	sum PROC v1, v2, v3
	    mov eax, v1
	    add eax, v2
	    add eax, v3
	    ret
	sum ENDP
end
; main.asm 直接引用编译后的lib文件即可
;这里的引入路径可以是全路径, 这里是相对路径
includelib /masm32/lib/sum.lib

;子程序声明
sum proto :dword, :dword, :dword
.code
	main PROC
		invoke sum,10,20,30    ;调用过程
		ret
	main ENDP
END main

结构与联合

结构(struct)时逻辑上互相关联的一组变量的模板或模式,结构中的单个变量称为域(field),程序的语句可以把结构作为一个实体进行访问,也可以对结构的单个域进行访问,结构通常包括不同类型的域,而联合(union)同样也是把多个标识符组合在一起,不过与结构不同的是,联合体共用用一块内存区域,内存的大小取决于联合体中最大的元素.

引用结构变量: 通过使用<>,{}均可声明结构体,同时可以初始化,对结构体赋初值.

;定义结构
MyPoint struct
	pos_x DWORD ?
	pos_y DWORD ?
MyPoint ends

.data
	;声明结构, 使用 <>、{} 均可
	ptr1 MyPoint <10,20>
	ptr2 MyPoint {30,40}

.code
	main PROC
		lea edx, ptr1
		mov eax, (MyPoint ptr [edx]).pos_x   ; 此时eax=10
		mov ebx, (MyPoint ptr [edx]).pos_y   ; 此时ebx=20
		mov (MyPoint PTR [edx]).pos_x,100    ; 将100写入MyPoint.pos_x结构中存储
		ret
	main ENDP
END main

结构初始化: 以下定义了MyStruct结构,并将user2初始化,FName=lyshark,FAge=25.

MyStruct struct
	FName db 20 dup(0)
	FAge db 100
MyStruct ends

.data
	user1 MyStruct <>
	user2 MyStruct <'lyshark',25>

.code
	main PROC
		;lea edx, user1
		;mov eax,DWORD PTR (MyStruct ptr[edx]).FName
		;mov ebx,DWORD PTR (MyStruct ptr[edx]).FAge

		mov eax,DWORD PTR [user2.FName]   ; eax=lyshark
		mov ebx,DWORD PTR [user2.FAge]    ; ebx=25
		ret
	main ENDP
END main

使用系统结构: 通过调用GetLocalTime获取系统时间,并存储到SYSTEMTIM结构体中.

.data
	sysTime SYSTEMTIME <>           ; 声明结构体

.code
	main PROC
		invoke GetLocalTime,addr sysTime    ; 获取系统时间并放入sysTime
		mov eax,DWORD PTR sysTime.wYear     ; 获取年份
		mov ebx,DWORD PTR sysTime.wMonth    ; 获取月份
		mov ecx,DWORD PTR sysTime.wDay      ; 获取天数
		ret
	main ENDP
END main

结构体的嵌套定义:

MyPT struct
	pt_x DWORD ?
	pt_y DWORD ?
MyPT ends
Rect struct
	Left MyPT <>
	Right MyPT <>
Rect ends

.data
	LyShark1 Rect <>
	LyShark2 Rect {<10,20>,<100,200>}
.code
	main PROC
		mov [LyShark1.Left.pt_x],100
		mov [LyShark1.Left.pt_y],200
		
		mov [LyShark1.Right.pt_x],1000
		mov [LyShark1.Right.pt_y],2000
		mov eax,[LyShark1.Left.pt_x]
		ret
	main ENDP
END main

联合体的声明:

; 定义联合体
MyUnion union
	My_Dword DWORD ?
	My_Word WORD ?
	My_Byte BYTE ?
MyUnion ends

.data
	test1 MyUnion {1122h}; ;只能存放初始值
.code
	main PROC
		mov eax, [test1.My_Dword]
		mov ax, [test1.My_Word]
		mov al, [test1.My_Byte]
		ret
	main ENDP
END main

关于宏汇编

宏过程(Macro Procedure)是一个命名的语汇编语句块,一旦定义后,宏过程就可以在程序中被调用任意多次,调用宏过程的时候,宏内的语句块将替换到调用的位置,宏的本质是替换,但像极了子过程,宏可定义在源程序的任意位置,但一般放在.data前面.

一个简单的宏:

MyCode macro
	xor eax,eax
	xor ebx,ebx
	xor ecx,ecx
	xor edx,edx
endm

.code
	main PROC
		MyCode   ; 将被替换为上面两行代码
		ret    
	main ENDP
END main

一个代替求和函数的宏

MySum macro  var1, var2, var3
	mov eax,var1
	add eax,var2
	add eax,var3
endm

.code
	main PROC
		MySum 10,20,30
		MySum 10,20,30,40   ; 多余的参数40会被忽略
		ret    
	main ENDP
END main

宏参数的默认值: 通过定义默认值,可以不给默认的变量传递参数.

; 参数 var1、var2 通过 REQ 标识说明是必备参数
MySum macro  var1:req, var2:req, var3:=<30>    ; var3默认值是30
	mov eax,var1
	add eax,var2
	add eax,var3
endm

.code
	main PROC
		MySum 10,20
		ret    
	main ENDP
END main

使用EXITM终止宏执行: 可使用关键字exitm 终止宏代码的后面内容.

MySum macro
	xor eax,eax
	xor ebx,ebx
	xor ecx,ecx
	exitm        ; 只会清空前三个寄存器,后面的跳过了
	xor edx,edx
	xor esi,esi
endm

.code
	main PROC
		MySum
		ret    
	main ENDP
END main

使用PURGE取消指定宏的展开:

MySum macro
	xor eax,eax
	xor ebx,ebx
endm

.code
	main PROC
		MySum           ; 这个会被展开
		purge MySum     ; 这个不会展开
		MySum           ; 这个宏也不会展开了
		ret
	main ENDP
END main

在宏内使用局部标号:

MyMax macro var1,var2
	LOCAL jump
	
	mov eax,var1
	cmp eax,var2
	jge jump
	xor eax,eax
jump:	ret
endm

.code
	main PROC
		MyMax 20,10
	main ENDP
END main

特殊操作符: &、<>、%、!

&  ;替换操作符
<> ;字符串传递操作符
%  ;表达式操作符, 也用于得到一个变量或常量的值
!  ;转义操作符
;自定义的宏
mPrint macro Text
    PrintText '* &Text& *'
endm

.code
main proc
    ;该宏会把参数直接替换过去
    mPrint 1234    ;* 1234 *
    
    ;要保证参数的完整应该使用 <>
    mPrint 12,34   ;* 12 *
    mPrint <12,34> ;* 12,34 *
    
    ;需要计算结果应该使用 %()
    mPrint 34+12   ;* 34+12 *
    mPrint %(34+12)   ;* 46 *
    
    ;用到 &、<、>、%、! 应该使用 ! 转义
    mPrint 10 !% 2 = %(10/2)!! ;* 10 % 2 = 5! *
    ret
main endp
end main

过程小例子

整数求和: 通过使用汇编语言实现一个整数求和的小例子.

.data
	String WORD 100h,200h,300h,400h,500h
.code
	main PROC
		;lea edi,String           ; 取String数组的基址
		mov edi,offset String     ; 同上,两种方式均可
		mov ecx,lengthof String   ; 取数组中的数据个数
		mov ax,0                  ; 累加器清零
	L1:
		add ax,[edi]              ; 加上一个整数
		add edi,TYPE String       ; 指向下一个数组元素,type(2byte)
		loop L1

		push 0
		call ExitProcess
	main ENDP
END main

正向复制字符串: 使用汇编语言实现字符串的复制,将数据从source复制到target内存中.

.data
	source BYTE "hello lyshark welcome",0h
	target BYTE SIZEOF source DUP(0),0h       ; 取源地址数据大小
.code
	main PROC
		mov esi,0                  ; 使用变址寄存器
		mov ecx,sizeof source      ; 循环计数器
	L1:
		mov al,source[esi]         ; 从源地址中取一个字符
		mov target[esi],al         ; 将该字符存储在目标地址中
		inc esi                    ; 递增,将指针移动到下一个字符
		loop L1

		push 0
		call ExitProcess
	main ENDP
END main

反向复制字符串: 使用汇编语言实现字符串的复制,将数据从source复制到target内存中且反向存储数据.

.data
	source BYTE "hello lyshark welcome",0h
	target BYTE SIZEOF source DUP(0),0h
.code
	main PROC
		mov esi,sizeof source
		mov ecx,sizeof source
		mov ebx,0
	L1:
		mov al,source[esi]
		mov target[ebx],al
		dec esi
		inc ebx
		loop L1
		push 0
		call ExitProcess
	main ENDP
END main

查看内存与寄存器: 通过调用DumpMem/DumpRegs显示内存与寄存器的快照.

.data
	array DWORD 1,2,3,4,5,6,7,8,9,0ah,0bh
.code
	main PROC
		mov esi,offset array       ; 设置内存起始地址
		mov ecx,lengthof array     ; 设置元素数据,偏移
		mov ebx,type array         ; 设置元素尺寸(1=byte,2=word,4=dword)
		call DumpMem               ; 调用内存查询子过程
		call DumpRegs              ; 调用查询寄存器子过程

		push 0
		call ExitProcess
	main ENDP
END main

汇编实现性能度量: 通过调用库函数,实现对指定代码执行的性能度量.

.data
	StartTime DWORD ?
.code
	main PROC

		call GetMseconds       ; 调用区本地时间过程
		mov StartTime,eax      ; 将返回值赋值给StartTime

		mov ecx,10             ; 通过调用延时过程,模拟程序的执行
	L1:
		mov eax,1000           ; 指定延时1s=1000ms
		call Delay             ; 调用延时过程
		loop L1

		call GetMseconds       ; 再次调用本地时间过程
		sub eax,StartTime      ; 结束时间减去开始时间
		call WriteDec          ; 以十进制形式输出eax寄存器的值

		push 0
		call ExitProcess
	main ENDP
END main

字符输出: WriteString(字符串),WriteInt(整数),WriteHex(16进制),WriteChar(字符),WriteDec(10进制).

.data
	Message BYTE "Input String:",0h
	String DWORD ?

.code
	main PROC
		; 设置控制台背景颜色
		mov eax,yellow +(blue*16)     ; 设置为蓝底黄字
		call SetTextColor             ; 调用设置过程
		call Clrscr                   ; 清除屏幕,clear

		; 提示用户一段话
		mov edx,offset Message        ; 指定输出的文字
		call WriteString              ; 调用回写过程
		call Crlf                     ; 调用回车

		push 0
		call ExitProcess
	main ENDP
END main

字符输入: ReadString(字符串),ReadInt(整数),ReadHex(16进制),ReadChar(字符),ReadDec(10进制).

.data
	Buffer BYTE 21 DUP(0)          ; 输入缓冲区
	ByteCount DWORD ?              ; 存放计数器      
.code
	main PROC
		mov edx,offset Buffer      ; 指向缓冲区指针
		mov ecx,sizeof Buffer      ; 指定最多读取的字符数
		call ReadString            ; 读取输入字符串
		mov ByteCount,eax          ; 保存读取的字符数

		push 0
		call ExitProcess
	main ENDP
END main

生成伪随机数:

.code
	main PROC
		mov ecx,5           ; 循环生成5个随机数
	L1:
		call Random32       ; 生成随机数
		call WriteDec       ; 以十进制显示
		mov al,TAB          ; 水平制表符
		call WriteChar      ; 显示水平制表符
		loop L1
		call Crlf           ; 回车

		push 0
		call ExitProcess
	main ENDP
END main

生成自定义随机数:

.code
	main PROC
		mov ecx,5           ; 循环生成5个随机数
	L1:
		mov eax,100         ; 0-99之间
		call RandomRange    ; 生成随机数
		sub eax,50          ; 范围在-50-49
		call WriteInt       ; 十进制输出
		mov al,TAB
		call WriteChar      ; 输出制表符
		loop L1
		call Crlf           ; 回车

		push 0
		call ExitProcess
	main ENDP
END main
posted @ 2019-07-05 09:17  lyshark  阅读(1626)  评论(0编辑  收藏  举报

loading... | loading...
博客园 - 开发者的网上家园