둘 다 “이 변수가 메모리의 어디에 있는가”를 레지스터에 넣을 때 쓴다. 가르는 질문은 “그 주소를 프로그램을 돌리기 전에 알 수 있는가”다. 알 수 있으면 OFFSET, 돌려 봐야 알면 LEA다.

  • OFFSET은 어셈블러가 번역할 때 주소를 계산해 상수로 박아 넣는 연산자다. 실행될 때는 이미 숫자다. 그래서 번역할 때 자리가 정해진 것(.data의 변수)에만 쓸 수 있다.
  • LEA는 실행될 때 CPU가 주소를 계산하는 명령이다. [ebp-8]처럼 레지스터 값에 따라 달라지는 주소도 구한다.

스택 매개변수와 지역 변수는 EBP에서의 거리로만 정해진다. EBP는 프로시저가 불릴 때마다 다를 수 있다. 그래서 이 둘의 주소를 얻으려면 LEA를 써야 한다.

CopyString PROC,
    count:DWORD
    LOCAL temp[20]:BYTE

    mov edi,OFFSET count      ; 오류: 스택 매개변수
    mov esi,OFFSET temp       ; 오류: 지역 변수
    lea edi,count             ; 된다
    lea esi,temp              ; 된다

이름을 붙이지 않고 [ebp-8]에 있는 지역 변수라면 이렇게 쓴다.

mov esi,OFFSET [ebp-8]        ; 오류
lea esi,[ebp-8]               ; ESI = EBP - 8
검증: 같은 식 [ebp-8]의 주소가 바깥 호출에서는 00000FF0h, 그 안에서 불린 호출에서는 00000FE0h로 다르다. 스택이 다른 곳에서 시작하면 또 달라진다. 전역 변수의 OFFSET은 그대로다. lea는 주소를, mov는 그 자리의 값을 넣는다 — 45_offset-vs-lea_verify.py (실제 어셈블러가 아니라 파이썬 흉내로 확인)

어느 쪽일까

C1 (a) .data에 msg BYTE "Hello",0이 있다. 이 문자열의 주소를 EDX에 넣는다. (b) 프로시저 안에서 LOCAL buf[20]:BYTE로 만든 배열의 주소를 ESI에 넣는다. 각각 어떻게 쓰는가?

답: (a) mov edx,OFFSET msg. (b) lea esi,buf.
이유: msg의 자리는 번역할 때 정해진다. buf의 자리는 이 프로시저가 불린 순간의 EBP에서 20바이트 아래이고, EBP는 실행해 봐야 안다. (a)를 lea edx,msg로 써도 같은 주소가 들어간다1.

C2 (c) 스택 매개변수 count:DWORD의 값을 ECX에 넣는다. (d) 같은 매개변수의 주소를 EDI에 넣는다. 어느 쪽에 LEA가 필요한가?

답: (c)는 주소가 아니라 값이 필요하므로 mov ecx,count(곧 mov ecx,[ebp+8])다. (d)는 lea edi,count다. mov edi,OFFSET count는 오류다.
이유: MOV는 그 자리에 든 값을 읽고, LEA는 그 자리가 어디인지만 계산한다. [ebp-8]에 55h가 들어 있고 그 주소가 0FF4h일 때 mov는 55h를, lea는 0FF4h를 넣는다1.

결정적 차이

  OFFSET LEA
정체 연산자 (어셈블러가 번역할 때 계산) 명령 (CPU가 실행할 때 계산)
결과 상수 그 순간의 레지스터 값으로 계산한 주소
전역 변수(.data) 된다 된다
스택 매개변수, 지역 변수 안 된다 된다
[ebp-8], [esi+4] 같은 식 안 된다 된다
쓰는 꼴 mov esi,OFFSET 이름 lea esi,피연산자

선택을 가르는 줄은 “결과”다. 지역 변수의 주소가 상수일 수 없다는 것은 프로시저를 두 깊이에서 불러 보면 드러난다. 바깥 프로시저와 그 안에서 불린 프로시저가 똑같이 sub esp,8 뒤 lea reg,[ebp-8]을 하면, 바깥은 00000FF0h를, 안쪽은 00000FE0h를 얻는다. 식은 같은데 값이 다르다. 번역할 때 하나의 숫자로 정해 둘 수가 없다1.

둘 다 아닐 때

C3 주소를 구하는 것처럼 보이지만 OFFSET도 LEA도 직접 쓰지 않는 경우 둘을 쓰라.

답: ① INVOKE로 프로시저를 부르면서 변수의 주소를 인수로 넘길 때는 ADDR을 쓴다(INVOKE Sub2,ADDR byteVal). ② 주소가 아니라 그 자리의 값이 필요하면 MOV를 쓴다. 이미 주소가 레지스터에 들어 있으면 [esi]로 따라가 읽는다.

연결

  1. 수치는 실제 어셈블러가 아니라 파이썬 흉내로 확인했다(검증 줄의 코드). ↩ ↩2 ↩3