곰투 크립토
guide전체 41편 중 39편

EVM 메모리 완벽 정리: 레이아웃·확장 가스·Solidity 안전 수칙

EVM 메모리의 수명과 확장 가스 계산, Solidity 프리 메모리 포인터, 인라인 어셈블리에서 피해야 할 오류를 설명합니다.

GOMTU
GOMTU
크립토 리서치 · 2026년 9월 15일 · 약 7분
공유𝕏in
EVM 메모리 완벽 정리: 레이아웃·확장 가스·Solidity 안전 수칙

Solidity 함수는 단순해 보여도 예상보다 많은 가스를 쓰거나, 직접 작성한 어셈블리 때문에 엉뚱하게 동작할 수 있어요. 이때 놓치기 쉬운 작업 공간이 **EVM 메모리(EVM Memory)**입니다. 메모리를 이해하면 실용적인 블록체인 기초와 컨트랙트가 읽고, 바꾸고, 해시하고, 반환한 뒤 버리는 바이트를 연결해서 볼 수 있습니다.

호출마다 새 모눈종이 두루마리를 받는다고 생각해 보세요. 컨트랙트는 종이 어디든 메모할 수 있지만, 더 멀리 펼칠수록 가스를 냅니다. 호출이 끝나면 종이는 폐기돼요. 이 글에서는 Solidity가 기대하는 메모리 레이아웃, 현재 확장 비용 공식, 저수준 코드를 검토할 때 지켜야 할 안전 경계를 설명합니다.

EVM 메모리란 무엇인가요?

광고

컨트랙트 실행 중 EVM은 수명이 서로 다른 여러 데이터 영역을 사용합니다. 이더리움 EVM 공식 문서는 메모리를 일시적인 바이트 주소 기반 작업 공간으로 설명해요. 256비트 스택(Stack), 영구적인 컨트랙트 스토리지(Storage)와 별개입니다.

각 메시지 호출 프레임(Call Frame)은 독립된 메모리를 받아요. 자식 호출이 부모 메모리를 직접 공유하지는 않습니다. 호출 명령은 지정된 입력 바이트를 자식 실행 맥락으로 복사하고, 실행 뒤 반환 데이터를 다시 복사해요. 새 프레임의 메모리는 0으로 시작하고 성공이나 리버트(Revert) 여부와 관계없이 프레임이 끝나면 사라집니다.

데이터 영역일반적인 용도쓰기 가능 여부수명
스택(Stack)연산자와 작은 값가능현재 호출 프레임
메모리(Memory)변경 가능한 임시 바이트가능현재 호출 프레임
콜데이터(Calldata)외부 호출 입력불가현재 호출 프레임
스토리지(Storage)컨트랙트 상태가능트랜잭션 이후에도 유지
트랜지언트 스토리지임시 컨트랙트 상태가능현재 트랜잭션

이 차이는 의미와 비용을 모두 바꿉니다. 참조형 데이터를 calldatastorage에서 memory로 옮기면 독립된 복사본이 생겨요. 반면 메모리 변수끼리 대입하면 같은 객체를 가리키는 또 다른 참조가 만들어질 수 있습니다. 정확한 규칙은 Solidity의 데이터 위치와 대입 동작 문서에서 확인할 수 있어요.

Solidity 메모리 레이아웃은 어떻게 생겼나요?

EVM 자체에는 평평한 바이트 배열이 있습니다. Solidity는 컴파일된 코드와 인라인 어셈블리가 같은 의미로 영역을 사용하도록 규칙을 더해요. 공식 메모리 레이아웃 문서는 처음 128바이트를 다음처럼 예약합니다.

바이트 범위Solidity 규칙
0x00–0x3f해싱 같은 짧은 연산을 위한 64바이트 스크래치 공간
0x40–0x5f프리 메모리 포인터(Free Memory Pointer)가 들어 있는 워드
0x60–0x7f빈 동적 배열의 초기 포인터로 쓰는 제로 슬롯(Zero Slot)
0x80…일반 할당 영역

0x40에 저장된 값은 처음에 0x80을 가리킵니다. 고수준 Solidity는 새 객체를 현재 프리 포인터 위치에 놓고 포인터를 앞으로 옮겨요. 호출 중 메모리를 해제하는 내장 기능은 없습니다.

동적 메모리 배열은 32바이트 길이 값으로 시작하고 원소가 뒤따릅니다. 대부분의 배열 원소는 Solidity 자료형이 더 작아도 32바이트의 배수를 차지하지만 bytesstring은 촘촘하게 저장되는 예외예요. 고정 길이 메모리 배열에는 길이 워드가 없습니다. 작은 값 여러 개가 하나의 영구 슬롯을 나눠 쓸 수 있는 이더리움 컨트랙트 스토리지 슬롯과는 배치 규칙이 달라요.

Warning

프리 메모리 포인터 이후의 미사용 영역이 항상 0이라고 가정하면 안 됩니다. Solidity는 포인터를 옮기지 않은 채 그 이후 공간을 임시로 쓸 수 있고, 공식 문서도 이 영역이 0으로 초기화되어 있다고 기대하지 말라고 경고해요.

MLOAD·MSTORE와 가장 높은 바이트 규칙

세 가지 기본 명령을 보면 구조가 선명해집니다.

  • MLOAD(offset)offset부터 32바이트를 읽어요.
  • MSTORE(offset, value)는 32바이트 워드 전체를 씁니다.
  • MSTORE8(offset, value)value의 가장 낮은 바이트 하나를 써요.

명령이 현재 활성 크기 밖의 비어 있지 않은 범위에 접근하면 메모리가 확장됩니다. 새 크기는 32바이트 워드 경계로 올림해요. 몇 바이트만 쓰려 했는지가 아니라 도달한 가장 높은 바이트가 확장량을 결정합니다. 아주 큰 오프셋 한 곳에 쓰는 것만으로도 그 지점까지 모든 메모리를 요구해 가스 부족(Out of Gas)이 날 수 있어요.

콜데이터나 반환 데이터를 복사하고, 메모리 영역을 해시하고, 로그 데이터를 내보내고, 다른 컨트랙트를 호출하거나, 결과 또는 리버트 데이터를 반환할 때도 메모리를 사용합니다. 각 명령 자체의 비용이나 워드별 비용은 아래의 메모리 확장 추가 비용과 별도로 계산돼요.

EVM 메모리 확장 가스는 어떻게 계산하나요?

현재 이더리움 EVM 규칙에서 활성 메모리 크기가 32바이트 워드 a개일 때 누적 비용은 다음과 같습니다.

Cmem(a) = 3 × a + floor(a² / 512)

이더리움 옐로 페이퍼가 이 공식을 정의합니다. 한 명령은 접근 전후 누적 비용의 차이만 지불해요.

확장 비용 = Cmem(확장 후 워드 수) - Cmem(확장 전 워드 수)

따라서 조금씩 여러 번 늘린다고 최종 누적 확장 비용을 피할 수는 없습니다. 이차항은 22워드까지 0이고 23워드부터 처음 생겨요. 가장 높은 접근 오프셋이 커질수록 영향도 커집니다.

활성 메모리워드 수누적 Cmem 가스
128바이트412
704바이트2266
736바이트2370
1,024바이트3298
4,096바이트128416

이 숫자는 순수한 EVM 메모리 확장 가스이지 사용자가 내는 전체 트랜잭션 수수료가 아닙니다. 실제 비용에는 다른 모든 명령, 콜데이터, 상태 접근, 사용 가스, 기본 수수료(Base Fee), 우선 수수료(Priority Fee)가 함께 반영돼요. 전체 가스비는 더 넓은 실행 맥락까지 포함해 계산해야 합니다.

공식 실행 명세 구현도 같은 워드 올림, 선형항, 이차항, 차액 부과 방식을 코드로 표현합니다. 포크 규칙은 바뀔 수 있으므로 가스에 민감한 도구는 블로그 공식을 영구 상수로 취급하지 말고 대상 네트워크의 활성 명세를 확인해야 해요. 예를 들어 EIP-7686은 선형 메모리 한도를 제안하지만 상태가 Stagnant입니다. 위에서 설명한 현재 적용 규칙이 아니에요.

인라인 어셈블리에서 안전하게 할당하는 패턴

Solidity 인라인 어셈블리 문서는 핵심 할당 방식을 다음처럼 보여줍니다.

function allocate(uint256 length) pure returns (uint256 pos) {
    assembly ("memory-safe") {
        pos := mload(0x40)
        mstore(0x40, add(pos, length))
    }
}

실제 할당 코드는 이후 객체가 워드 정렬을 요구한다면 바이트 길이를 올림하고, 덧셈이 넘치지 않는지도 확인해야 합니다. 핵심 순서는 단순해요. 포인터를 읽고, 겹치지 않는 영역을 예약한 뒤, Solidity가 다른 객체를 같은 자리에 놓기 전에 포인터를 갱신합니다.

memory-safe 표시는 런타임 안전장치가 아닙니다. 해당 블록이 Solidity 관리 메모리, 직접 올바르게 할당한 메모리, 0x00–0x3f 스크래치 영역 또는 조건에 맞는 포인터 이후 임시 영역만 건드린다는 컴파일러와의 약속이에요. 컴파일러는 이 약속을 믿고 추가 최적화를 적용할 수 있습니다. 거짓으로 표시하면 테스트에서 쉽게 드러나지 않는 잘못된 미정의 동작이 생길 수 있어요.

리스크와 흔한 실수

예약 슬롯을 덮어쓰기

0x40에 잘못된 값을 남기면 이후 할당이 겹칠 수 있습니다. 0x60 제로 슬롯에 쓰면 빈 동적 배열의 기본 표현이 깨질 수 있어요. 스크래치 공간은 잠깐만 쓰는 곳이므로 고수준 연산을 지나서도 값이 남는다고 기대하면 안 됩니다.

별칭과 복사본을 혼동하기

두 메모리 변수가 같은 객체를 가리킬 수 있어요. 한 참조로 값을 바꾸면 다른 참조에서 보이는 값도 달라질 수 있습니다. 반대로 메모리·콜데이터·스토리지 경계를 넘으면 복사가 생길 수 있어요. 변수 이름만 보고 추측하지 말고 언어의 대입 규칙을 확인하세요.

공격자가 정하는 오프셋까지 확장하기

검증하지 않은 입력이 어셈블리 오프셋이나 길이를 결정하면 한 번의 접근으로 극단적인 확장을 강제해 가스 부족을 일으킬 수 있습니다. 덧셈과 메모리 접근 전에 범위를 제한하세요. 평균 입력뿐 아니라 최악 입력으로 테스트해야 해요.

임시 메모리를 트랜잭션 공유 상태로 보기

메모리는 호출 프레임 하나에 속합니다. 중첩 호출이나 콜백 전체가 공유하는 잠금 장치로 쓸 수 없어요. 호출 프레임을 넘되 한 트랜잭션 동안만 유지할 상태가 필요하다면 트랜지언트 스토리지의 다른 의미와 보안 조건을 검토해야 합니다.

실전 검토 체크리스트

  • 각 참조 값이 memory, calldata, storage 중 어디에 있는지 구분하기
  • 0x40 프리 포인터를 유효하게 유지하고 0x60 제로 슬롯 보존하기
  • 동적 데이터를 쓰기 전에 겹치지 않는 영역 예약하기
  • 사용자 입력 오프셋과 길이를 연산 전에 제한하기
  • 가스 분석에 복사 비용과 확장 차액을 함께 반영하기
  • memory-safe를 검사 기능이 아닌 컴파일러와의 약속으로 보기
  • 큰 배열, 리버트 데이터, 반환 데이터, 중첩 호출 테스트하기
  • 대상 포크와 컴파일러 버전에서 가스 가정 다시 확인하기

자주 묻는 질문 (FAQ)

EVM 메모리는 영구적으로 남나요?

아니요. 호출 프레임에 속하고 프레임이 끝나면 사라집니다. 영구적인 컨트랙트 값은 스토리지에 두고, 트랜잭션 범위에서 여러 호출이 공유할 임시 상태는 용도에 맞을 때 트랜지언트 스토리지를 사용해요.

메모리는 항상 스토리지보다 저렴한가요?

두 영역의 용도가 다릅니다. 메모리는 영구 상태 쓰기를 피하지만 복사와 확장에도 가스가 들어요. 크거나 듬성듬성한 접근은 비쌀 수 있습니다. 보편적인 구호를 적용하기보다 컴파일된 바이트코드와 실제 입력을 비교하세요.

메모리를 읽기만 해도 확장되나요?

네. 길이가 0이 아닌 읽기가 현재 활성 크기를 넘어가면 확장됩니다. 필요한 가장 높은 바이트를 기준으로 32바이트 워드 단위 올림을 적용해요.

Solidity 메모리를 호출 중 해제할 수 있나요?

현재 고수준 Solidity는 포인터를 앞으로 이동하며 할당하고 객체를 해제하지 않습니다. 호출이 반환되거나 리버트되면 해당 프레임의 메모리 전체가 사라져요.

주요 출처

메모리를 제한된 실행 자원으로 보세요

EVM 메모리는 일시적이지만 공짜도, 구조 없는 공간도 아닙니다. Solidity 포인터 규칙은 객체가 서로 충돌하지 않게 하고, 이더리움 확장 비용은 프레임이 도달한 가장 높은 영역에 값을 매겨요. 데이터 위치를 구분하고, 프리 포인터를 지키고, 오프셋과 길이를 제한한 뒤 활성 포크에서 컴파일된 실행 경로를 측정하세요.

이 글은 교육 목적이며 투자 조언이 아닙니다. 스마트 컨트랙트 버그와 크립토 자산은 전액 손실을 일으킬 수 있어요. 최신 1차 문서를 확인하고 배포하거나 서명하기 전에 테스트하며, 잃어도 감당할 수 있는 범위만 사용하고 스스로 조사하세요(DYOR).

광고

함께 읽으면 좋은 글

관련 주제 살펴보기

GOMTU의 다른 글