zoukankan      html  css  js  c++  java
  • 常用的ARM汇编指令

    转自:https://blog.csdn.net/zb861359/article/details/81027021?utm_source=app

    1、  IMPORT和EXPORT

    IMPORT ,定义表示这是一个外部变量的标号,不是在本程序定义的

    EXPORT ,表示本程序里面用到的变量提供给其他模块调用的。

    以上两个在汇编和C语言混合编程的时候用到。

    2、AREA

    语法格式:    
        AREA 段名 属性1 ,属性2 ,……    
        AREA伪指令用于定义一个代码段或数据段。其中,段名若以数字开头,则该段名需用“|”括起来,如:|1_test|。    
        属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下:    
        — CODE 属性:用于定义代码段,默认为READONLY 。    
        — DATA 属性:用于定义数据段,默认为READWRITE 。    
        — READONLY 属性:指定本段为只读,代码段默认为READONLY 。    
        — READWRITE 属性:指定本段为可读可写,数据段的默认属性为READWRITE 。    
        — ALIGN 属性:使用方式为ALIGN表达式。在默认时,ELF(可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为0~31,相应的对齐方式为2表达式次方。    
        — COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的COMMON段共享同一段存储单元。 
        一个汇编语言程序至少要包含一个段,当程序太长时,也可以将程序分为多个代码段和数据段。    
        使用示例:    
        AREA Init ,CODE ,READONLY ;   该伪指令定义了一个代码段,段名为Init ,属性为只读。

    3、LDR、LDRB、LDRH

    ARM微处理器支持加载/存储指令用于在寄存器和存储器之间传送数据,加载指令用于将存储器中的数据传送到寄存器,存储指令则完成相反的操作。常用的加载存储指令如下:

    —  LDR     字数据加载指令

    — LDRB    字节数据加载指令

    —  LDRH    半字数据加载指令

    1)     LDR指令有两种用法:

    a、ldr加载指令
    LDR指令的格式为:
    LDR{条件}  目的寄存器,<存储器地址>
    LDR指令用亍从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据迕行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。该指令在程序设计中比较常用,丏寻址方式灵活多样,请读者认真掌握。
    指令示例:
    LDR R0,[R1]        ;将存储器地址为R1的字数据读入寄存器R0。
    LDR R0,[R1,R2]  ;将存储器地址为R1+R2的字数据读入寄存器R0。
    LDR R0,[R1,#8]   ;将存储器地址为R1+8的字数据读入寄存器R0。
    LDR R0,[R1,R2]!;将存储器地址为R1+R2的字数据读入寄存器R0,幵将新地址R1+R2写入R1。
    LDR R0,[R1,#8]!  ;将存储器地址为R1+8的字数据读入寄存器R0,幵将新地址R1+8写入R1。 
    LDR R0,[R1],R2  ;将存储器地址为R1的字数据读入寄存器R0,幵将新地址R1+R2写入R1。
    LDR R0,[R1,R2,LSL#2]!  ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
    LDR R0,[R1],R2,LSL#2  ;将存储器地址为R1的字数据读入寄存器R0,幵将新地址R1+R2×4写入R1。”

    ARM是RISC结构,数据从内存到CPU间的移劢只能通过L/S指令来完成,也就是ldr/str指令。  
    比如想把数据从内存中某处读取到寄存器中,只能使用ldr 
    比如: 
    ldr r0, 0x12345678 
    就是把0x12345678返个地址中的值存放到r0中。

    b、ldr伪指令
    ARM指令集中,LDR通常都是作加载指令的,但是它也可以作伪指令。
    LDR伪指令的形式是“LDRRn,=expr”。
    例子:
    COUNT EQU       0x40003100
    ……
    LDR       R1,=COUNT
    MOV      R0,#0
    STR       R0,[R1]

    COUNT是我们定义的一个变量,地址为0x40003100。这种定义方法在汇编语言中是很常见的,如果使用过单片机的话,应该都熟悉这种用法。
    LDR       R1,=COUNT是将COUNT这个变量的地址,也就是0x40003100放到R1中。
    MOV      R0,#0是将立即数0放到R0中。最后一句STR     R0,[R1]是一个典型的存储指令,将R0中的值放到以R1中的值为地址的存储单元去。实际就是将0放到地址为0x40003100的存储单元中去。

    下面还有一个例子
    ;将COUNT的值赋给R0
    LDR       R1,=COUNT
    LDR       R0,[R1]
    LDR       R1,=COUNT这条伪指令,是怎样完成将COUNT的地址赋给R1,有兴趣的可以看它编译后的结果。这条指令实际上会编译成一条LDR指令和一条DCD伪指令。

    2)     LDRB指令

    LDRB指令的格式为:

    LDR{条件}B 目的寄存器,<存储器地址>

    LDRB指令用于将存储器中低8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的数据被当作目的地址,从而可以实现程序流程的跳转。

    指令示例:

    LDRB R0,[R1]         ;将存储器地址为R1的字节数据读入寄存器R0,并将R0的高24位清零。

    LDRB R0,[R1,#8]    ;将存储器地址为R1+8的字节数据读入寄存器R0,并将R0的高24位清零。

    3)     LDRH指令

    LDRH指令的格式为:

    LDR{条件}H 目的寄存器,<存储器地址>

    LDRH指令用于将存储器中低16位的半字数据传送到目的寄存器中,同时将寄存器的高16位清零,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

    指令示例:

    LDRH R0,[R1]         ;将存储器地址为R1的半字数据读入寄存器R0,并将R0的高16位清零。

    LDRH R0,[R1,#8]    ;将存储器地址为R1+8的半字数据读入寄存器R0,并将R0的高16位清零。

    LDRH R0,[R1,R2]    ;将存储器地址为R1+R2的半字数据读入寄存器R0,并将R0的高16位清零。

    4、SUB

    (Subtraction)

    SUB{条件}{S} , , dest = op_1 - op_2

    SUB 用操作数 one 减去操作数 two,把结果放置到目的寄存器中。操作数 1 是一个寄存器,操作数 2 可以是一个寄存器,被移位的寄存器,或一个立即值:

    SUB R0, R1, R2 ;R0 = R1 - R2

    SUB R0, R1, #256; R0 = R1 - 256

    SUB R0, R2,R3,LSL#1 ; R0 = R2 - (R3 << 1)

    减法可以在有符号和无符号数上进行。

    ps:带进位的减法SBC

    5、CMP 、TST、BNE、BEQ

    BNE和BEQ经常与CMP 或TST搭配使用。

    在讲解这几个指令前,先介绍下CPSR这个寄存器,因为CMP和TST指令会对CPSR中的某些位产生影响,而BNE和BEQ指令的动作正是与CPSR中被影响的这些位有关。

    CMP:

    假设现在AX寄存器中的数是0002H,BX寄存器中的数是0003H。执行的指令是:CMP AX, BX

    执行这条指令时,先做用AX中的数减去BX中的数的减法运算。列出二进制运算式子:
           0000 0000 0000 0010

    -0000 0000 0000 0011
    _________________________________
    (借位1) 1111 11111111 1111

    所以,运算结果是 0FFFFH

    根据这个结果,各标志位将会被分别设置成以下值:
    CF=1,因为有借位   // CF即为上述CPSR中的C
    OF=0,未溢出           // OF即为上述CPSR中的V
    SF=1,结果是负数   // SF即为上述CPSR中的N
    ZF=0,结果不全是零    // ZF即为上述CPSR中的Z

    还有AF, PF等也会相应地被设置。

    CMP 比较指令做了减法运算以后,根据运算结果设置了各个标志位。

    标志位设置过以后,0FFFFH这个减法运算的结果就没用了,它被丢弃,不保存。

    执行过了CMP指令以后,除了CF,ZF,OF,SF,等各个标志位变化外,其它的数据不变。

    对照普通的减法指令 SUB AX, BX,它们的区别就在于:
    SUB指令执行过以后,原来AX中的被减数丢了,被换成了减法的结果。
    CMP指令执行过以后,被减数、减数都保持原样不变。

    TST:

    逻辑处理指令,用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位与运算,并根据运算结果更新CPSR中条件标志位的值。当前运算结果为非0,则Z=0;当前运算结果为0,则Z=1

    BNE:数据跳转指令,标志寄存器中Z标志位等于零时,跳转到BNE后标签处。特别要注意这里,通常我们说BNE是“不相等”或“不为零”时跳转,此处的“不相等”或“不为零”是指比较结果不相等或做减法不为零,不是指Z标志位。比较结果不相等或做减法不为零时,Z标志位是等于零的。

    BEQ:数据跳转指令,标志寄存器中Z标志位不等于零时, 跳转到BEQ后标签处

    6、STR、STRB、STRH指令

    — STR     字数据存储指令

    — STRB    字节数据存储指令

    — STRH    半字数据存储指令

    A、STR指令的格式为:

    STR{条件} 源寄存器,<存储器地址>

    STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用,且寻址方式灵活多样,使用方式可参考指令LDR。

    指令示例:

    STR   R0,[R1],#8    ;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。

    STR   R0,[R1,#8]    ;将R0中的字数据写入以R1+8为地址的存储器中。

    B、STRB指令的格式为:

    STR{条件}B 源寄存器,<存储器地址>

    STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。

    指令示例:

    STRB R0,[R1]         ;将寄存器R0中的字节数据写入以R1为地址的存储器中。

    STRB R0,[R1,#8]    ;将寄存器R0中的字节数据写入以R1+8为地址的存储器中。

    C、STRH指令的格式为:

    STR{条件}H 源寄存器,<存储器地址>

    STRH指令用于从源寄存器中将一个16位的半字数据传送到存储器中。该半字数据为源寄存器中的低16位。

    指令示例:

    STRH R0,[R1]         ;将寄存器R0中的半字数据写入以R1为地址的存储器中。

    STRH R0,[R1,#8]    ;将寄存器R0中的半字数据写入以R1+8为地址的存储器中。

    7、跳转指令B、BL、BX、BLX 和 BXJ的区别

    跳转指令用于实现程序流程的跳转,在 ARM 程序中有两种方法可以实现程序流程的跳转:

    (1) 使用专门的跳转指令。

    (2) 直接向程序计数器 PC 写入跳转地址值。

    通过向程序计数器 PC 写入跳转地址值,可以实现在4GB 的地址空间中的任意跳转,在跳转之前结合使用

    MOV LR , PC

    等类似指令,可以保存下一条指令地址作为将来的返回地址值,从而实现在 4GB 连续的线性地址空间的子程序调用。

    专门的跳转指令:

    B、BL、BX、BLX 和 BXJ

    跳转、带链接跳转(带返回的跳转)、跳转并切换指令集、带链接跳转并切换指令集(带返回的跳转并切换指令集)、跳转并转换到 Jazelle 状态。

    a)     、 B 指令

    B 指令的格式为:

    B{条件} 目标地址

    B 指令是最简单的跳转指令。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(前后32MB 的地址空间)。以下指令:

    B Label ;程序无条件跳转到标号 Label 处执行

    CMP R1 ,# 0 ;当 CPSR 寄存器中的 Z 条件码置位时,程序跳转到标号Label 处执行

    BEQ Label

    b)     、 BL 指令

    BL 指令的格式为:

    BL{条件} 目标地址

    BL 是另一个跳转指令,但跳转之前,会在寄存器R14 中保存PC 的当前内容,因此,可以通过将R14 的内容重新加载到PC 中,来返回到跳转指令之后的那个指令处执行。该指令是实现子程序调用的一个基本但常用的手段。以下指令:

    BL Label ;当程序无条件跳转到标号 Label 处执行时,同时将当前的 PC 值保存到 R14 中

    c)     、 BLX 指令

    BLX 指令的格式为:

    BLX 目标地址

    BLX 指令从ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态有ARM 状态切换到Thumb 状态,该指令同时将PC 的当前内容保存到寄存器R14 中。因此,当子程序使用Thumb 指令集,而调用者使用ARM 指令集时,可以通过BLX 指令实现子程序的调用和处理器工作状态的切换。

    同时,子程序的返回可以通过将寄存器R14 值复制到PC 中来完成。

    d)     、 BX 指令 

    BX 指令的格式为:

    BX{条件} 目标地址

    BX 指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM 指令,也可以是Thumb指令。

    语法

    op1{cond}{.W}<wbr />label
    op2{cond} <wbr />Rm

    其中:

    op1

    是下列项之一:

    B:跳转;BL:带链接跳转;BLX:带链接跳转并切换指令集。

    op2

    是下列项之一:

    BX:跳转并切换指令集;BLX:带链接跳转并切换指令集;

    BXJ:跳转并转换为Jazelle执行。

    cond:是一个可选的条件代码。 cond 不能用于此指令的所有形式。

    .W:是一个可选的指令宽度说明符,用于强制要求在 Thumb-2 中使用 32 位 B 指令。

    label:是一个程序相对的表达式。

    Rm:是一个寄存器,包含要跳转到的目标地址。

    所有这些指令均会引发跳转,或跳转到 label,或跳转到包含在Rm中的地址处。此外:

    BL 和 BLSTMCSIAX 指令可将下一个指令的地址复制到 lr(r14,链接寄存器)中。

    BX 和 BLX 指令可将处理器的状态从 ARM 更改为 Thumb,或从 Thumb 更改为 ARM。

    BLX label 无论何种情况,始终会更改处理器的状态。

    BX Rm 和 BLX Rm 可从 Rm 的位 [0] 推算出目标状态:

    如果 Rm 的位 [0] 为 0,则处理器的状态会更改为(或保持在)ARM 状态

    如果 Rm 的位 [0] 为 1,则处理器的状态会更改为(或保持在)Thumb 状态。

    BXJ 指令会将处理器的状态更改为 Jazelle

    8、STMFD和LDMFD指令

    这两条指令分别是入栈和出栈的意思,因此先来讲一下堆栈的相关概念:

    a)     满堆栈:即入栈后堆栈指针sp指向最后一个入栈的元素。也就是sp先减一(加一)再入栈。

    b)     空堆栈:即入栈后堆栈指针指向最后一个入栈元素的下一个元素。也就是先入栈sp再减一(或加一)。

    c)     递增堆栈:即堆栈一开始的地址是低地址,向高地址开始递增。就如同一个水杯(假设上面地址大)开口的是大地址,从杯底开始装水。

    d)     递减堆栈:即堆栈一开始的地址是高地址,向低地址开始递增。就如同刚才说的那个水杯,现在开口的是小地址,从大地址开始用。

    有这些类型就可以构成4种不同的堆栈方式,arm的栈一般我们用满堆栈、递减堆栈。

    一开始,看到 STMFD sp!{R0-R5,LR} 这条命令时真是有点疑惑。STMFD的意思是:ST(store 存储) M(multiple 多次)F(full 满堆栈)D(decrease 递减堆栈),合起来就是按满的递减的方式把后面的寄存器里的值都存到sp中。

    STMFD sp!{R0-R5,LR}就是把lr  r5-r0 依次存到sp中,并且sp会在存数据之前自动减一个数据的空间(因为arm栈是递减的)。至于最后一个问题,就是sp后为什么有一个“!”。如果有!号,表示在存入数据后sp会指向最后一个存入的数据的地址,否则sp会把自己的值加到一开始的地址。(就是sp在执行完这条指令之后sp指向的地址不变)。

    例子:STMFD sp!,{r0} ;将r0中的值压入堆栈,压入过程是,由于r0中的值为32位的,首先将sp减去4(因为arm栈是递减的),将r0中的低八位放入sp这个位置,第九位到第十六位放入sp+1的地址,将第十七位到第二十四位放入sp+2的位置,将第二十五位到第三十二位放入sp+3的位置。

    LDMFD sp!,{r2,r3};将堆栈中的内容出栈,出栈过程是,将sp这个位置的值放入r2中的低八位,将sp+1这个位置的值放入r2中的第九位到第十六位,将sp+2这个位置的值放入r2中的第十七位到第二十四位,将sp+3这个位置的值放入r2中的第二十五位到第三十二位;将sp+4这个位置的值放入r3中的低八位,将sp+5这个位置的值放入r3中的第九位到第十六位,将sp+6这个位置的值放入r3中的第十七位到第二十四位,将sp+4这个位置的值放入r3中的第二十五位到第三十二位。最后sp=sp+8。

    此外,STR指令也可以用来入栈:strr1, [sp,#4] ,将r1中的值压入堆栈,压入过程是,由于r1中的值为32位的,将r0中的低八位放入sp+4这个位置,第九位到第十六位放入sp+5的地址,将第十七位到第二十四位放入sp+6的位置,将第二十五位到第三十二位放入sp+7的位置。

    9、移位指令(操作)

    a)     LSL(或ASL)操作

    LSL(或ASL)操作的格式为:

    通用寄存器,LSL(或ASL) 操作数     

    LSL(或ASL)可完成对通用寄存器中的内容进行逻辑(或算术)的左移操作,按操作数所指定的数量向左移位,低位用零来填充。其中,操作数可以是通用寄存器,也可以是立即数(0~31)。

    操作示例

    MOV  R0, R1,LSL#2              ;将R1中的内容左移两位后传送到R0中。

    b)     LSR操作

    LSR操作的格式为:

    通用寄存器,LSR 操作数     

    LSR可完成对通用寄存器中的内容进行右移的操作,按操作数所指定的数量向右移位,左端用零来填充。其中,操作数可以是通用寄存器,也可以是立即数(0~31)。

    操作示例:

    MOV  R0, R1, LSR#2           ;将R1中的内容右移两位后传送到R0中,左端用零来填充。

    c)     ASR操作

    ASR操作的格式为:

    通用寄存器,ASR 操作数     

    ASR可完成对通用寄存器中的内容进行右移的操作,按操作数所指定的数量向右移位,左端用第31位的值来填充。其中,操作数可以是通用寄存器,也可以是立即数(0~31)。

    操作示例:

    MOV   R0, R1, ASR#2          ;将R1中的内容右移两位后传送到R0中,左端用第31位的值来填充。

    d)     ROR操作

    ROR操作的格式为:

    通用寄存器,ROR 操作数     

    ROR可完成对通用寄存器中的内容进行循环右移的操作,按操作数所指定的数量向右循环移位,左端用右端移出的位来填充。其中,操作数可以是通用寄存器,也可以是立即数(0~31)。显然,当进行32位的循环右移操作时,通用寄存器中的值不改变。

    操作示例:

    MOV   R0, R1, ROR#2           ;将R1中的内容循环右移两位后传送到R0中。

    e)     RRX操作

    RRX操作的格式为:

    通用寄存器,RRX 操作数     

    RRX可完成对通用寄存器中的内容进行带扩展的循环右移的操作,按操作数所指定的数量向右循环移位,左端用进位标志位C来填充。其中,操作数可以是通用寄存器,也可以是立即数(0~31)。

    操作示例:

    MOV  R0, R1,RRX#2             ;将R1中的内容进行带扩展的循环右移两位后传送到R0中。

    f)       SHL和SHR:逻辑移位指令。

    SHL是逻辑左移指令,它的功能为:

    (1)将一个寄存器或内存单元中的数据向左移位;

    (2)将最后移出的一位写入CF中;

    (3)最低位用0补充。

    指令:
    MOV AL,01001000b

    SHL AL,1 ;将AL中数据左移一位

    执行后(AL)=10010000b,CF=0。

    注意:

    如果移动位数大于1时,必须将移动位数放在CL中。

    比如,指令:

    MOV AL,01010001b

    MOV CL,3

    SHL AL,CL

    执行后(AL)=10001000b,因为最后移出的一位是0,所以CF=0。LDRB

    SHR是逻辑右移指令,它和SHL所进行的操作刚好相反。

    10、逻辑指令

    a)     AND

    逻辑与操作指令。将operand2 值与寄存器Rn 的值按位作逻辑与操作,结果保存到Rd 中。指令格式如下:
            AND{cond}{S} Rd,Rn,operand2
            AND 指令举例如下:
            ANDS R0,R0,#x01 ;R0=R0&0x01,取出最低位数据
            AND R2,R1,R3 ;R2=R1&R3

    b)     ORR
            逻辑或操作指令。将operand2 的值与寄存器Rn 的值按位作逻辑或操作,结果保存到Rd 中。指令格式如下:
            ORR{cond}{S} Rd,Rn,operand2
            ORR 指令举例如下:
            ORR R0,R0,#x0F ;将R0 的低4 位置1
            MOV R1,R2,LSR #4
            ORR R3,R1,R3,LSL #8 ;使用ORR 指令将近R2 的高8 位数据移入到R3 低8 位中

    c)     EOR
            逻辑异或操作指令。将operand2 的值与寄存器Rn 的值按位作逻辑异或操作,结果保存到Rd 中。指令格式如下:
            EOR{cond}{S}Rd,Rn,operand2
            EOR 指令举例如下:
            EOR R1,R1,#0x0F ;将R1 的低4 位取反
            EOR R2,R1,R0 ;R2=R1^R0
            EORS R0,R5,#0x01 ;将R5 和0x01 进行逻辑异或,结果保存到R0,并影响标志位

    11、条件助记符

    ARM汇编指令的基本指令格式如下:

    <opcode>[<cond>][s]<Rd>,<Rn>,[<op2>],其中,[<参数>]可选,指令长度32bit。第一项为操作码,第二项为条件助记符。常用的条件助记符及其含义如下表所示。由指令格式可知,操作码可与条件助记符结合起来一起使用,如MOVEQ,MOVNE,BLS等等,根据下表中条件助记符的含义和操作码的含义,对于上述组合命令也就不难理解了。

    操作码

    条件助记符

    标志

    含义

    0000

    EQ

    Z=1

    相等

    0001

    NE

    Z=0

    不相等

    0010

    CS/HS

    C=1

    无符号数大于或等于

    0011

    CC/LO

    C=0

    无符号数小于

    0100

    MI

    N=1

    负数

    0101

    PL

    N=0

    正数或零

    0110

    VS

    V=1

    溢出

    0111

    VC

    V=0

    没有溢出

    1000

    HI

    C=1,Z=0

    无符号数大于

    1001

    LS

    C=0,Z=1

    无符号数小于或等于

    1010

    GE

    N=V

    有符号数大于或等于

    1011

    LT

    N!=V

    有符号数小于

    1100

    GT

    Z=0,N=V

    有符号数大于

    1101

    LE

    Z=1,N!=V

    有符号数小于或等于

    1110

    AL

    任何

    无条件执行 (指令默认条件)

    1111

    NV

    任何

    从不执行(不要使用)

  • 相关阅读:
    通俗易懂,什么是.NET?什么是.NET Framework?什么是.NET Core?(转)
    Yii Framework2.0开发教程(5)数据库mysql性能
    假设动态运行java文字,当在脚本式配置,这是非常方便的
    采用jquery的imgAreaSelect样品图像裁剪示范插件实现
    Mybatis 构造resultMap 搜sql
    第13周项目1-动物所谓的
    旋转华尔兹
    馋-c语言的规则
    毕业后的第二份工作:进入国外 在新加坡工作 每月一次18K
    Bringing up interface eth0: Device eth0 does not seem to be present, delaying initialization
  • 原文地址:https://www.cnblogs.com/lgslearn/p/14206465.html
Copyright © 2011-2022 走看看