요약

두 줄로 선 사람들 가운데 앞줄만 옆으로 밀고, 생긴 빈자리를 뒷줄 사람이 건너와 채우는 밀기다. 뒷줄은 사람을 빌려줄 뿐 자기 자리는 그대로다. 보통 시프트가 빈자리를 0으로 채워 정보를 잃는 것과 달리, 이웃한 값에서 비트를 끌어오므로 레지스터 둘에 나눠 담은 긴 수를 끊김 없이 밀 수 있다. 다만 8비트 값에는 쓸 수 없고, 뒷줄은 스스로 움직이지 않으므로 긴 수 전체를 밀려면 뒷줄을 따로 한 번 더 밀어야 한다.

예시로 보기

64비트 수는 32비트 레지스터 하나에 안 들어가서 EDX(위쪽 절반)와 EAX(아래쪽 절반)에 나눠 담는다. 이 수를 왼쪽으로 4칸 밀려면 EAX의 맨 위 4비트가 EDX의 맨 아래로 넘어가야 한다. shl edx,4만 하면 EDX의 아래 4칸에 0이 들어와 그 4비트를 잃는다.

SHLD는 그 빈칸을 다른 레지스터의 위쪽 비트로 채운다. 16비트 값으로 해 보면 이렇다.

.data
wval WORD 9BA6h
.code
mov  ax,0AC36h
shld wval,ax,4      ; wval = BA6Ah, AX는 AC36h 그대로
            wval            AX
밀기 전   [9][B][A][6]   [A][C][3][6]
              ↙  ↙  ↙  ↖────┘            AX의 맨 위 4비트(A)가 건너온다
밀고 난 뒤 [B][A][6][A]   [A][C][3][6]    맨 위의 9는 밀려 나갔다

16진 한 자리가 4비트이므로 4칸 밀기는 16진 한 자리 옮기기다. wval의 9가 밀려 나가고, 맨 아래 빈자리에 AX의 맨 위 자리 A가 들어왔다.

검증: 본문의 모든 값과 이어서 실행하기의 답(FA67h, 36FAh), 무작위 16비트 2만 쌍에서 아래 두 식, 8비트 전수 검사, 64비트 수를 shld와 shl로 민 결과 — 40_double-shift_verify.py (실제 어셈블러가 아니라 파이썬 흉내로 확인)

정의

SHLD 대상, 원천, 칸수는 대상을 왼쪽으로 민다. 아래쪽에 생긴 빈칸을 원천의 맨 위 비트들로 채운다. SHRD 대상, 원천, 칸수는 대상을 오른쪽으로 민다. 위쪽에 생긴 빈칸을 원천의 맨 아래 비트들로 채운다. 두 명령 모두 원천은 바뀌지 않는다.

쓸 수 있는 피연산자는 다음뿐이다. 대상은 16비트나 32비트 레지스터 또는 메모리, 원천은 같은 크기의 레지스터, 칸 수는 8비트 즉시값이나 CL이다.

SHLD reg16/32, reg16/32, imm8/CL
SHLD mem16/32, reg16/32, imm8/CL
SHRD reg16/32, reg16/32, imm8/CL
SHRD mem16/32, reg16/32, imm8/CL

기호로 쓰면, 대상 \(d\)와 원천 \(s\)가 \(w\)비트이고 \(n\)칸(\(1 \le n < w\))을 밀 때

\[\mathrm{SHLD}(d, s, n) = \left\lfloor \frac{(d \cdot 2^w + s) \cdot 2^n}{2^w} \right\rfloor \bmod 2^w, \qquad \mathrm{SHRD}(d, s, n) = \left\lfloor \frac{s \cdot 2^w + d}{2^n} \right\rfloor \bmod 2^w\]

이다. \(d \cdot 2^w + s\)는 대상을 위에, 원천을 아래에 이어 붙인 \(2w\)비트 수다. SHLD는 이 긴 수를 왼쪽으로 \(n\)칸 민 뒤 위쪽 절반을 읽은 것이다. SHRD는 반대로 원천을 위에, 대상을 아래에 이어 붙인 수를 오른쪽으로 \(n\)칸 민 뒤 아래쪽 절반을 읽은 것이다. \(\bmod 2^w\)는 \(w\)비트만 남긴다는 뜻이다. CF에는 대상에서 마지막으로 밀려난 비트가 들어간다1.

예제

SHRD. AX를 오른쪽으로 4칸 밀고, 맨 위 빈자리를 DX의 맨 아래 4비트로 채운다.

mov  ax,234Bh
mov  dx,7654h
shrd ax,dx,4        ; AX = 4234h, DX는 7654h 그대로
             DX             AX
밀기 전   [7][6][5][4]   [2][3][4][B]
                    └────↘  ↘  ↘  ↘      DX의 맨 아래 4비트(4)가 건너온다
밀고 난 뒤 [7][6][5][4]   [4][2][3][4]    맨 아래의 B는 밀려 나갔다

한 칸짜리 그림. 한 칸 미는 모습을 8칸짜리로 줄여 그리면 이렇다.

  대상 (밀기 전) 원천 대상 (민 뒤) CF
SHLD, 1칸 11100000 10011101 11000001 1
SHRD, 1칸 11000001 00011101 11100000 1

SHLD에서는 원천의 맨 위 비트 1이 대상의 맨 아래로 왔다. SHRD에서는 원천의 맨 아래 비트 1이 대상의 맨 위로 왔다. 실제 명령은 8비트 레지스터를 받지 않는다. 이 표는 움직임만 보이려고 8칸으로 줄인 것이고, 코드에서는 shld ax,bx,1처럼 16비트 이상을 쓴다.

이어서 실행하기. 두 명령을 차례로 실행한다. 둘째 줄은 첫째 줄이 바꿔 놓은 DX에서 시작한다.

명령 민 뒤 DX AX
mov ax,7C36h / mov dx,9FA6h 9FA6h 7C36h
shld dx,ax,4 FA67h 7C36h
shrd dx,ax,8 36FAh 7C36h

첫 줄: DX의 9가 나가고 AX의 맨 위 7이 아래로 들어와 FA67h. 둘째 줄: FA67h의 아래 두 자리 67이 나가고 AX의 아래 두 자리 36이 위로 들어와 36FAh. 둘째 줄을 처음 DX(9FA6h)에 바로 하면 369Fh로 다른 값이 나온다1.

활용

  • 레지스터 둘에 걸친 수 밀기. EDX:EAX에 든 64비트 수를 왼쪽으로 4칸 미는 코드다. 순서가 중요하다. EAX를 먼저 밀면 넘겨줄 위쪽 4비트가 이미 사라진다1.
shld edx,eax,4      ; EAX의 맨 위 4비트를 EDX 아래로 받는다
shl  eax,4          ; 그다음 EAX 자신을 민다

EDX = 12345678h, EAX = 9ABCDEF0h이면 결과는 EDX = 23456789h, EAX = ABCDEF00h다.

  • 한 칸씩이면 회전으로도 된다. shl eax,1 다음 rcl edx,1은 한 칸을 같은 식으로 민다. SHLD는 여러 칸을 명령 하나로 넘겨준다는 점이 다르다. 회전 명령
  • 흔한 실수: 원천도 같이 밀린다고 생각하는 것, 원천 자리에 메모리나 즉시값을 쓰는 것(레지스터만 된다), SHRD의 빈칸이 원천의 맨 위 비트로 찬다고 생각하는 것(맨 아래 비트다).
  • 연습: 시프트와 회전 예제 사다리

연결

확인 문제

C1 SHLD와 SHRD가 각각 빈칸을 원천의 어느 쪽 비트로 채우는지, 실행 뒤 원천이 어떻게 되는지 쓰라.

답: SHLD는 대상을 왼쪽으로 밀고 아래쪽 빈칸을 원천의 맨 위 비트들로 채운다. SHRD는 대상을 오른쪽으로 밀고 위쪽 빈칸을 원천의 맨 아래 비트들로 채운다. 원천은 둘 다 그대로다.

C2 AX = 1234h, BX = ABCDh다. shld ax,bx,8을 실행한 결과와, 같은 처음 값에서 shrd ax,bx,8을 실행한 결과를 각각 쓰라.

답: shld ax,bx,8 → AX = 34ABh. shrd ax,bx,8 → AX = CD12h. BX는 두 경우 모두 ABCDh.
이유: 8칸은 16진 두 자리다. SHLD: AX의 12가 나가고 BX의 위 두 자리 AB가 아래로 들어온다. SHRD: AX의 34가 나가고 BX의 아래 두 자리 CD가 위로 들어온다1.

C3 EDX:EAX의 64비트 수를 왼쪽으로 4칸 밀 때 shl eax,4를 먼저 하고 shld edx,eax,4를 나중에 하면 왜 틀리는가?

답: shl eax,4가 EAX의 맨 위 4비트를 버리고 나면, shld가 EDX로 받아 오는 것은 원래 맨 위 4비트가 아니라 그 아래에 있던 4비트다. 넘겨줘야 할 비트를 넘기기 전에 잃는다.
이유: SHLD는 원천의 “지금” 맨 위 비트를 읽는다. 원천을 바꾸지 않는 명령이므로 원천을 미는 일은 그 뒤에 따로 한다1.

  1. 수치는 검증 줄의 파이썬 흉내로 확인했다. ↩ ↩2 ↩3 ↩4 ↩5 ↩6