어셈블리

어셈블리 강좌7

RSMaster 2019. 7. 9. 20:10

제8 장. 반복 기법
--------------------------------------------------------------------------------

8.1 루프(loop)명령
--------------------------------------------------------------------------------
비교와 분기를 하나로 합쳐서 한 명령으로 할 수 있도록 된  루프(loop), 스트링(string)

LOOP 명령
LOOP 명령은 CX레지스터를 뺄셈식 카운터로서 사용하고 , CX 레지스터가 0 이 될 때까지
어떤 문장들을 되풀이 반복한 뒤 지정한 라벨로 분기하는 명령입니다. 즉, CX에 설정
한 횟수만큼 반복을 하기 위한 명령인 것입니다.
LOOP명령은 오퍼랜드에 분기하는 곳의 라벨을 지정하여,

        LOOP    NEXT

와 같은 식으로 사용합니다. 이명령은 지금까지 알고 있는 명령을 조합하여 실행하려고
하면
        DEC     CX
        CMP     CX,0
        JNE     NEXT

와 같이 하면 됩니다. 즉 , LOOP 명령은 CX 레지스터 뺄셈 명령과 비교 명령과  조건 분기
명령을 한 명령으로 실행하는 명령입니다.  이경우  뺄셈 카운터로서 사용되는  것은 
CX레지스터라고 정해져 있습니다. 루프 명령을 실행하면 CX 레지스터 값을 하나 빼고,
0 이안면 지정된 라벨로 분기합니다.
LOOP명령에 의해 CX레지스터에 지정된 횟수만큼 반복할 수가 있습니다.
CX 레지스터에는 LOOP명령에서 지정하는 반복 범위보다 앞서서 반복하는 횟수를 설정해
주지 않으면 안 됩니다.

예제) LOOP1.ASM
1부터 10H까지의 수치를 화면에 출력하는 프로그램
CODE    SEGMENT
        ASSUME  CS:CODE, DS:CODE
;
INCLUDE PUTAL2.SUB   - INCLUDE파일의 전개 부분의 리스트는 생략하였다.
;
START:  PUSH    CS   -+DS에 CS의 값을 지정함
        POP     DX   -+
;
        MOV     AX,1
        MOV     CX,10H
L1:     MOV     DS, AL  -+
        CALL    PUTAL   |
        CALL    SPACE   | 이범 위를 10H회 반복한다.
        INC     AX      |
        LOOP    L1     -+
        MOV     AH,4CH
        INT     21H
;
SPACE   PROC    NEAR   - NEAR지정은 없어도 좋다.
        PUSH    AX     -+ 레지스터를 퇴피
        PUSH    DX     -+

        MOV     DL, ' ' -+공백 문자를 출력
        MOV     AH,2    |
        INT     21H    -+
        POP     DX     -+퇴피했던 레지스터 값을 회복
        POP     AX     -+
        RET
SPACE   ENDP
;
CODE    ENDS
        END     START

위의 프로그램은 리스트는 INCLUDE 파일 이 전개된  부분은 생략하고 나타낸 것입니다
CX레지스터의 초기 설정은 루프 밖에서 세트
LOOP명령은 , 그 자신 속에 CX의 값을 빼는 명령을 포함하고 있다
LOOP : 무조건 지정된 횟수를 반복
LOOPE(loop while equal) , LOOPNE(loop while not equal):
어떤 조건이 지정된 횟수 이내에 서 반복을 끝냄
이 명령 앞에 비교 명령(또는 연산 명령)을 놓고 그 결과가 같고 , 또한 CX 레지스터의 1
씩을 값을 뺀 결과가 0 이 아니면 반복하는 명령입니다.
상한을 CX 회로 설정해 놓고 같지 않은 것이 나타날 때까지 반복하는 명령입니다.
이것은 데이터의 열중에서 서로 다른 데이터를 찾는데에 사용할 수 있습니다.


예제) LOOP2.ASM
 문자열 "_ _ _ _ _HELLO!"의 공백의 개수를 세고, 문자열 "HELLO!_ _  _"중의 연속된 
문자 개수를 세어 출력한다.
CODE    SEGMENT
        ASSUME  CS:CODE, DS:DATA
INCLUDE PUTAL2.SUB  -+INCLUDE파일의 전개 부분은 생략한다.
INCLUDE CRLF.SUB    -+
;
SPACE   EQU     ' '
;
START:  MOV     AX, DATA
        MOV     DS, AX
;
;COUNT  SPACE   LENGTH          공백 길이를 센다.
        MOV     CX,10H          상한 횟수를 10H회로 결정한다.
        MOV     DL,0            공백수를 DL레지스터에 카운트
        MOV     SI,0            선두 번지부터 몇 번째인가를 나타내는 SI
        MOV     BX, OFFSET STR1
NEXT1:  INC     DL
        INC     SI               SI 가 INC SI에 의해서 1부터 시작되므로
        MOV     AL, [BX+SI-1] --  하나를 빼야 한다.
        CMP     AL, SPACE         CX내용에서 1을 뺀 결과 0이 아닌 동안 여기서 AL과
        LOOPE   NEXT1            SPACE와 비교한 결과 같으면 NEXT1으로 분기한다.
        DEC     DL     -----+    같지 않으면 아래로 계속한다.
        MOV     AL, DL  --+  +--- 카운트에 더 더한 만큼 뺀다
        CALL    PUTAL  --+------ 표시
        CALL    CRLF   --------- 줄을 바꾼다.
;


;COUNT STRING LENGTH              문자 열겠소를 센다
        MOV     CX,10H
        MOV     DL,-1      초기값을 -1로 하는 것은
        MOV     SI,-1      0 FFFFH세트 하는 것
NEXT2:  INC     DL
        INC     SI
        MOV     AL, [BX+SI]
        CMP     AL, SPACE  -+ CX를 1 감소시켰을 때 0이 아닌 범위 내에서 AL와
        LOOPNE  NEXT2     -+ SPACE가 같지 않으면 NEXT2로 분기하여 문자 개수
        MOV     AL, DL를 센다.
        CALL    PUTAL   ---  표시
        CALL    CRLF         줄을 바꾼다.
;
        MOV     AH,4CH
        INT     21H
;
CODE    ENDS
DATA    SEGMENT
STR1    DB              HELLO!' ;선두가 공백인 문자
STR2    DB      'HELLOW!   '    ;선두부터 문자가 들어있는 문자
DATA    ENDS
        END     START

 

LOOP 명령과 플레그 레지스터:
LOOP 명령, LOOPE명령, LOOPNE명령은 모두 CX 레지스터를 카운터로 하고 한번 실행할 때
마다 CX 레지스터 값을 하나 씩 값을 빼고 있습니다. 그러나 여기에서 CX 레지스터 값
을 뺄 떼는 , 결과가 무엇이든 상관없이 플래그 레지스터의 값은 변화하지 않습니다.
즉, LOOP명령 등의 실행 후 플래그 레지스터의 값은 앞의 연산 결과를 유지하고 있으니
다. 따라서 LOOP 명령 중의  CX 레지스터를 빼는 명령은 단순한 DEC 명령과 치환할 수는
없습니다.(DEC  명령에서는   CF 이외의  레지스터가   변화한다.).  LOOP명령, LOOPE명
령, LOOPNE 명령을 이해를 돕기 위해서 다른 명령으로 치환한다면 다음과 같이 됩니다.

LOOP명령 LOOPE명령, LOOPNE명령들의 똑같은 기능을 다른 명령으로 치환
+-------------------------------------------------------+
|       LOOP L1         LOOPE L1        LOOPNE L1       |
|                                                       |
|       PUSHF           PUSHF           PUSHF           |
|       DEC   CX        JNZ   EXIT      JZ    EXIT      |
|       JE    EXIT      DEC   CX        DEC   CX        |
|                       JZ    EXIT      JZ    EXIT      |
|       POPF            POPF            POPF            |
|       JMP   L1        JMP   L1        JMP   L1        |
|  EXIT:POPF       EXIT:POPF       EXIT:POPF            |
+-------------------------------------------------------+
여기서 PUSHF는 플래그 레지스터(flag register)를 스택에 퇴피시킵니다.

 

8.2 스트링(string)  명령
--------------------------------------------------------------------------------
연속한 데이터를 전송하는 명령(5종류)
문자열과 같은 연속한 데이터를 전송하는 명령입니다.
리피트 프리픽스(repeat prefix) 명령과  조합하여 사용함으로써 , 한 명령으로  대량의 
데이터를 전송할 수 있는 이점이 있습니다.

   스트링 명령
+-----------+-------+-------------------+-------+------------------------+
|           |       |  바이트 단위      |       |     워드 단위          |
+-----------+-------+-------------------+-------+------------------------+
|전송 명령군| LODSB | 메모리 -> AL       | LODSW | 메모리 -> AX           |
|           | STOSB | AL     -> 메모리   | STOSW | AX     -> 메모리       |
|           | MOVSB | 메모리 ->메모리   | MOVSW | 메모리 -> 메모리       |
+-----------+-------+-------------------+-------+------------------------+
|비교 명령군| SCASB | 메모리 AL의 비교 | SCASW | 메모리와 AX의 비교      |
|           | CMPSB | 메모리끼리의 비교 | CMPSW | 메모리와 메모리의 비교 |
+-----------+-------+-------------------+-------+------------------------+

 

리피트 프리픽스 명령
+-------------+-------+--------------------------------------------------------+
| 전송 명령군 | REP   | CX  레지스터에 주어진 횟만큼, 다음의 스트링 명령을 반복 |
+-------------+-------+--------------------------------------------------------+
| 비교 명령군 | REPE |  비교 결과가 0 및  CX레지스터에 주어진 횟수 이내이면 다  |
|             | REPZ  |  음의 스트링 명령을 반복한다.                          |
|             +-------+--------------------------------------------------------+
|             | PEPNE |   비교결과가 0이 아니고 CX레지스터에 주어진 횟수 이 내이|
|             | PEPNZ |   내 이면 다음의 스트링 명령을 반복한다.               |
+-------------+-------+--------------------------------------------------------+

스트링 명령에서는 메모리의 번지를 지정하는 방법이
정해져 있고, 또한 데이터를 읽는 방법과 쓰는 방법이 차이가 있습니다.
*. 데이터를 읽어낼때...DS 내의 [SI]로 표시되는 번지의 내용을 읽어낸다.
*.데이터를 써넣을 때... ES 내의 [DI]로 표시되는 번지의 데이터를 써넣는다.
+----------+
| DS:[SI]  +-----+  LODS명령
+----------+    \|/
               +-----------------+
MOVS명령       |  AX 혹은 AL     |
(CMP명령)      +-----------------+
                 |
+----------+     |  STOS명령
| ES:[DI]  +<----+ ( SCAS 명령)
+----------+
사용하는 번지도 바이트 단위면 AL, 워드 단위이면 AX 로정해져 있습니다. 따라서 스트
링 명령을 사용하기 전에는 필요한 레지스터(DS, ES, SI, DI, CX)의 초기 설정을  하지 않은
면 안 되는 대신에, 명령 자체는 오퍼랜드를 가지지 않고 단순히
LODSB
MOVSW
와 같은 식으로 단순하게 사용할 수가 있습니다.

단, 스트링 명령은 전송이나 비교 등을 행한 다음, 번지 지정에 사용되어 SI혹은  DI레지
스터의 값을 자동적으로 1(바이트 단위일 때) 또는 2(워드 단위일때) 만큼  증가시키니
다.  이것은 다음 번지의 데이터를 전송 또는 비교하기 위한 준비이고, 스트링 명령을 연
속하여 사용함으로써 연속된 번지에 놓인 데이터를 전송 또는 비교할 수 있습니다.
(디렉션 플레그 값이 1 일 때는 , SI, DI 갑은 감소된다.)

LOOP 명령을 사용하기 위해, 편의상 미리 CX에 문자열의 길이를 넣어두지 않으면 안될
니다. 문자열의  길이를 계산하기 위하여  원하는 문자열의  다음에 더미(dummy)  변수 
MSG2를 놓아서 두 개의 오프셋    번지의 차를 취합니다.
OFFSET MSG2-OFFSET MSG

이와 같이 매크로 어셈블에서는 번지 등의 덧셈 뺄셈을 소스 프로그램 중에 쓸 수가  있
입니다. 이 방법은 정석으로 되어 있습니다.

LOODSB명령은 , 데이터를 전송한 다음 SI 값을 하나 증가시키므로, 한 문자 출력한  다
음 LOOP명령으로 돌아와서 다시 LODSB 명령을 실행하게 되면, SI는 앞에 출력한 문자
가 있는 번지의 다음 번지를 나타내고 있습니다.(연속 문자 출력 가능)

STOS명령
메모리로부터 메모리로 데이터 전송
LODS와 STOS를 조합하는 방법 , MOVS를 사용하는 방법,

 예제) MOVS2.ASM
MSG1에 들어 있는 스트링 FUNCTION 09H:DISPLAY STRING을
MSG2로 전송하는 펑션 9를 호출하여 출력한다.
CODE    SEGMENT
        ASSUME  CS:CODE, DS:DATA, ES:DATA
;
START:  MOV     AX, DATA
        MOV     DS, AX
        MOV     ES, AX
;
        MOV     CX, OFFSET MSG2-OFFSET MSG1
        MOV     SI, OFFSET MSG1
        MOV     DI, OFFSET MSG2
        CLD      ;DI의 증가 설정
NEXT:   LODSB    ;DS:[SI]로 정해지는 번지 메모리 내용을 ES:[DI]로
        STOSB    ;정해지는 메모리에 전송한다. 단, 문자열의 길이(CX) 만큼
        LOOP    NEXT
        MOV     BYTE PTR ES:[DI], '$'
        MOV     DX, OFFSET MSG2
        MOV     AH,9H
        INT     21H
;
CODE    ENDS

DATA    SEGMENT
MSG1    DB      'FUNCTION 09H:DISPLAY STRING'
MSG2    DB      20H DUP (?)
DATA    ENDS
        END     START


LODSB의 실행 후 SI의 값이 하나 증가하고,STOSB의 실행후 DI의 값이 하나 증가,
2 개의 명령에 LOOP 명령을 더함으로써 연속한 데이터를 전송할 수가 있습니다.


MOVS 명령:
위의 LODS 명령과 STOS 명령을 MOVS 명령으로 치환

CODE    SEGMENT
        ASSUME  CS:CODE, DS:DATA, ES:EXTRA
;
START:  MOV     AX, DATA
        MOV     DS, AX     ;DS설정
        MOV     AX, EXTRA  ;독립된 ES의 설정이 필요
        MOV     ES, AX
;
        CLD               ;DI의 증가 설정
        MOV     DX,5      ;5번 되풀이
        MOV     DI, OFFSET MSG2
NEXT2:  MOV     CX, OFFSET DMSG - OFFSET MSG1
        MOV     SI, OFFSET MSG1
NEXT:   MOVSB             ;CX의 값이 0이 될 때까지 MSG1번지부터 시작하는 스트링
        LOOP    NEXT      ;을 MSG2 번지 위치부터 옮겨 놓는다.
        DEC     DX        ;DX에서 1을 빼라
        JNZ     NEXT2     ;0 이 아니면 NEXT2로 가라.
        MOV     BYTE PTR ES:[DI], '$'   ;메모리에 $(문자열 끝 표시)를 넣는다.
        MOV     AX, ES
        MOV     DS, AX     ;펑션 호출 9번의 앞에는 DS의 설정이 필요
        MOV     DX, OFFSET MSG2
        MOV     AH,9H     ;DX에 지정된 번지부터 저장된 스트링을 $ 자가 나타
        INT     21H       ;날 때까지 출력하는 시스템 펑션 호출.
        MOV     AH,4CH
        INT     21H
;
CODE    ENDS
;
DATA    SEGMENT
MSG1    DB      'REPEAT SAME MESSAGE 5 TIMES!'
DMSG    DB?
DATA    ENDS
;
EXTRA   DB      100H DUP (?)
EXTRA   ENDS
        END     START

 

8.3 리피트 프리픽스(1)
--------------------------------------------------------------------------------
리피트 프리픽스 명령은 스트링 명령과 조합하여 사용하고 , 스트링 명령을 일정  횟수 
연속하여 사용하는 것입니다.
REP     MOVSB
-+-
 +-----> 이것이 리피트 프리픽스이다.
        CX에 지정된 횟수만큼 MOVSB를 되풀이하라는 의사 명령
        리피트 프리픽스는 스트링 명령 앞에 놓고, 스트링 명령의 기능 확장을 지정

여기서 STOSB --> B는 바이트를 나타냄
       STOSW --> W는 워드를 나타냄
따라서 AX에   AB 가 들어 있어도    STOSB는  B 만을 나타낸다.
AX 레지스터에 'AB'를 대입하고 있으므로, AL 레지스터의 내용은 'B'=42H가 됩니다.
워드 단위의 스트링 명령을 사용한 경우에는 20H 개의 바이트의 데이터가 전송됩니다.
이것을 실행하면
A> MOVS5
BABABABABABABABABABA
AX레지스터의 값 'AB'가 메모리 상에 전송 돌 경우에는 역 워드(reverse word) 형식으로
되기 때문에 , 'BA' 순서로 되어버린 점에 주의하십시오.

REP 명령은 조합하는 스트링 명령이 바이트 형인가 워드 형인가에 따라서 전송되는 바
이트 수는 달라집니다.
예를 들면 20H 바이트의 데이터를 전송할 때에 MOVSB명령을 사용한다고 하면 CX 레지
스터의 초기치를 20H로 , MOVSW명령을 사용한다고 하면 CX 레지스터의 초기치를 10H
토하지 않으면 안 될 것입니다.
전송하는 데이터의 수가 홀수 바이트 일 때에는 워드형의 스트링 명령을 사용하면 마지
막 데이터가 전송되니 않는다든지 , 여분의 데이터까지 전송된다든지 하는 일이 있을
므로 주의가 필요합니다.   미리 데이터수를 알과 있는 경우는 괜찮지만 경우에 따라 전
송하는 데이터수가 변화할 때에는 바이트 형의 스트링 명령을 사용하는 쪽이 안전  합
니다.

8.4 비교 명령군의 사용법
--------------------------------------------------------------------------------
메모리와 레지스터의 값을 비교하는  SCAS명령,
메모리끼리의 값을 비교하는         CMPS명령

SCAS명령
AL 레지스터의 값과 ES:[DI]에 의해 표시되는 메모리의 내용을 비교

몇 번 반복했는가를 알기 위해서 ,
원래의 CX값을 보존해두어, 루프에서 빠져나온 후의 CX 값을 빼면 된다.
문자열 길이의 계산 : STR2 - STR1
                    OFFSET STR2 - OFFSET STR1
과 완전히 같은 역할을 합니다. 이것도 하나의 정석으로서 외워두십시오


CODE    SEGMENT
        ASSUME  CS:CODE, DS:CODE, ES:CODE
;
INCLUDE PUTAL2.SUB
START:  MOV     AX, CODE  ;
        MOV     DS, AX
        MOV     ES, AX    ;DS와 ES의 설정
        CLD              ;DI의 증가 설정
        MOV     AL, '&'   ;문자열 중에서 찾는 문자
        MOV     DI, OFFSET STR1
        MOV     CX, STR2-STR1    ; 문자열의 길이를 계산하려면
        PUSH    CX              ;OFFSET을 생략해도 좋다.
NEXT:   SCASB------------+일치하는 문자가 있는가 찾는다.
        JE      FOUND    |(AL의 값과 ES:[DI]로 지정된 메모리 내용과 내용을 비교한
        LOOP    NEXT   --+ 다, 그리고 자동적으로 DI는 증가 CX는 감소한다.)
        JMP     NOTFOUND
FOUND:  DEC     DI
        MOV     DX, OFFSET FOUNDMSG1
        MOV     AH,9
        INT     21H
        MOV     AX, DI  --+- 발견한 번지를 표시
        MOV     AL, AH    |
        CALL    PUTAL    |
        MOV     AX, DI    |
        CALL    PUTAL  --+
        MOV     DX, OFFSET FOUNDMSG2
        MOV     AH,9
        INT     21H
        POP     AX     --+문자열의 길이-뺄셈 카운터에 의해 몇 번째 문자에서
        SUB     AX, CX    | 발견되었는지를 계산하여 표시
        CALL    PUTAL  --+
        JMP     EXITP
NOTFOUND:
        MOV     DX, OFFSET NOTFOUNDMSG
        MOV     AH,9
        INT     21H
EXITP:  MOV     AH,4CH
        INT     21H
;
FOUNDMSG1       DB      'FOUND ADDRESS = $'
FOUNDMSG2       DB      'H COUND = $'
NOTFOUNDMSG     DB      'NOT FOUND $'
STR1    DB      'ABCDEFG&ABC'
STR2    DB?
CODE    ENDS
        END     STRAT


CMPS 명령 :
2개의 데이터 열이 같은가 다른가를 비교하는 데 사용합니다.
DS:[SI]가 지정하는 메모리 한 바이트 내용과
ES:[DI]가 지정하는 메모리 한 바이트 내용을 비교하는 명령이다.
CMPSB명령을 하면 SI, DI의 값이 자동적으로 하나만큼 증가한다는  점입니다.

예제) CMPS2.ASM
메모리 번지 CMP-STRING부터 저장되어 있는 "ABC" 문자와 키보드로부터 입력하여
메모리 번지 BUFF부터 저장된 문자 스트링  속에 같은 "ABC"가 몇 번째 글자부터 있
는가 알아낸다.

CODE    SEGMENT
        ASSUME  CS:CODE, DS:CODE, ES:CODE
;
INCLUDE CRLF.SUB
INCLUDE PUTAL2.SUB
START:  MOV     AX, CS
        MOV     DS, AX
        MOV     ES, AX
        CLD
        MOV     DX, OFFSET OPENING_MSG
        CALL    DISP
        CALL    CRLF
        MOV     DX,OFFSET MAX_CHARS  -+리터 키가 눌릴 때까지 키보드로부터 입력된
        MOV     AH,0AH                |스트링을 연속적으로 BUFF속에 저장하고 총
        INT     21H                  -+입력된 문자 개수를 CHARS-ENTERED에 지정하
        CALL    CRLF는 시스템 펑션 A 번이다.
        MOV     CH,0
        MOV     CL, CHARS_ENTERED
        MOV     BP, OFFSET BUFF
        DEC     BP
        INC     CX
NEXT:   DEC     CX
        CMP     CX,3
        JB      NOTFOUND
        INC     BP
        MOV     DI, BP
        MOV     SI, OFFSET CMP_STRINGS
        CMPSB       <---+DS:[SI]로 지정되는 메모리 내용과  ES:[DI]로 지정되는
        JNE     NEXT    |메모리 내용을 비교한다. 비교한 후, 자동적으로 DI, SI값
        CMPSB       <---+을 증가시켜 다음 것을 비교한다.
        JNE     NEXT    |
        CMPSB       <---+
        JNE     NEXT
        MOV     DX, OFFSET FOUND_MSG1
        CALL    DISP                 -+몇 번째 문자에서 발견되었는지를 계산 표시
        MOV     AL, CHARS_ENTERED      |
        SUB     AL, CL                -+
        CALL    PUTAL
        JMP     EXITP
NOTFOUND:
        MOV     DX, OFFSET NOTFOUND_MSG
        CALL    DISP
EXITP:  MOV     AH,4CH
        INT     21H
DISP    PROC
        MOV     AH,09H
        INT     21H
        RET
DISP    ENDP
OPENING_MSG     DB      'INPUP STRINGS INCLUDE'
<ABC> $'
FOUND_MSG1      DB      'FOUND CHARACTERS AT $'
NOTFOUND_MSG    DB      'NOT FOUND $'

MAX_CHARS       DB      80
CHARS_ENTERED   DB?
BUFF            DB      80H DUP (0)
   +--> 최대 입력 가능 문자 개수(CR포함):1바이트
+--+---------+---------+----------------------+
|            |         |   버퍼             --+--> 입력된 실지 문자가 들어가는 곳
+------------+--+------+----------------------+
                +----> 실제로 입력된 문자 개수(CR 포함하지 않음) : 1바이트
CMP_STRINGS     DB      'ABC'------> 비교대상이 되는 문자열
CODE    ENDS
        END     START

위문제는 상당히 논리가 어려운 문제입니다. 그러나 꾸준히 추적해나가면 곧  이해할 수
있을 것입니다. 그러나 이 프로그램을 완전히 이해했다면 어셈블리 언어의 상당한 수준
을 파악하게 된 셈이며 앞으로 어떤  복잡한 어셈블리 언어 프로그램도 이해할 수 있는
능력이 생긴 것입니다.

'어셈블리' 카테고리의 다른 글

깔끔한 어셈 명령어 정리  (0) 2019.07.09
어셈 명령어 표형식 정리  (0) 2019.07.09
어셈블리 강좌6  (0) 2019.07.09
어셈블리 강좌5  (0) 2019.07.09
어셈블리 강좌4  (0) 2019.07.09