Smalltalk80LanguageImplementationKor:Chapter 27

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 27 장 가상 머신의 명세

가상 머신의 명세

26장에서는 인터프리터와 객체 메모리로 구성된 스몰토크 가상 머신의 기능을 설명하였다. 이번 장과 다음 3개의 장에서는 이러한 가상 머신의 두 가지 부분에 대해 좀 더 형식적인 명세를 제시하고자 한다. 가상 머신의 구현은 대부분 마이크로코드 또는 기계어로 작성될 것이다. 하지만 구체성을 위해 지금부터는 Smalltalk 자체에서 가상 머신의 구현을 제시할 것이다. 언제나처럼 세부적인 내용을 숨기지 않도록 최선을 다하겠다.


이번 장은 세 개의 절로 구성된다. 첫 번째 절은 형식적 명세에 사용되는 용어와 규칙을 설명한다. 또한 이러한 명세 형태로부터 발생할 수 있는 혼동과 관련된 경고를 몇 가지 제시할 것이다. 두 번째 절은 인터프리터가 사용한 객체 메모리 루틴을 설명한다. 이러한 루틴의 구현은 제 30장에서 설명할 것이다. 세 번째 절은 인터프리터가 조작하는 주요 객체 유형, 메서드, 컨텍스트, 클래스를 설명하고자 한다. 제 28장은 바이트코드 집합과 그것이 어떻게 해석되는지, 제 29장은 프리미티브 루틴을 설명한다.


명세 형태(From of the Specification)

Interpreter와 ObjectMemory라는 두 가지 클래스 기술이 Smalltalk-80 가상 머신의 형식적 명세를 구성한다. Interpreter의 구현은 이번 장과 28, 29장에서 상세히 제시할 것이며, ObjectMemory의 구현은 30장에서 살펴볼 것이다.


지금부터 혼란을 야기할 수 있는 부분은 이러한 기술에 관련된 두 가지 Smalltalk 시스템, 즉 Interpreter와 ObjectMemory를 포함하는 시스템과 해석되는 시스템에서 비롯된다. Interpreter와 ObjectMemory에는 메서드와 인스턴스 변수가 있으며, 이들 또한 해석하는 시스템 내에서 메서드와 인스턴스 변수를 조작한다. 혼동을 최소화하기 위해 각 시스템마다 다른 용어 집합을 사용할 것이다. Interpreter와 ObjectMemory의 메서드는 "routines"(루틴)이라고 부르고, "method"(메서드)라는 단어는 해석되는 메서드에 사용할 것이다. 또한 Interpreter와 ObjectMemory의 인스턴스 변수는 "registers"(레지스터)라고 부르고, "instance variable"(인스턴스 변수)는 해석되는 시스템 내 객체의 인스턴스 변수를 가리키도록 하겠다.


Interpreter와 ObjectMemory의 레지스터 내용과 루틴의 인자는 거의 대부분 Integer의 인스턴스가 될 것이다 (SmallIntegers와 LargePositiveIntegers). 해석되는 시스템에도 Integers가 있기 때문에 이 또한 혼란을 야기할 수 있다. 레지스터의 내용과 루틴에 대한 인자인 Integer는 해석되는 시스템의 수치값과 객체 포인터를 가리킨다. 이 중 일부는 해석되는 시스템에서 Integers의 값 또는 객체 포인터를 나타낼 것이다.


이러한 명세에서 인터프리터 루틴은 Smalltalk 메서드 정의의 형태로 되어 있을 것이다. 아래를 예로 들 수 있겠다.

routineName: argumentName
    | temporaryVariable |
    temporary Variable  self another Routine: argumentName.
    temporaryVariable - 1


명세 내 루틴은 5개 유형의 표현식을 포함할 것이다.

  1. 1. 인터프리터의 다른 루틴을 호출한다. 루틴의 정의와 호출 모두 Interpreter에 위치하므로 self에 대한 메시지로 나타날 것이다.
    • self headerOf: newMethod
    • self storeInstructionPointerValue: value
          inContext: contextPointer
  2. 2. 객체 메모리의 루틴을 호출한다. Interpreter는 그것의 객체 메모리를 참조하기 위해 memory란 이름을 사용하므로 이러한 호출은 memory에 대한 메시지로 나타날 것이다.
    • memory fetchClassOf: newMethod
    • memory storePointer: senderIndex
          ofObject: contextPointer
          withValue: activeContext
  3. 3. 객체 포인터와 수치값에 대한 산술 연산. 산술 연산은 표준 Smalltalk 산술 표현식으로 표현될 것이기 때문에 숫자 자체에 대한 메시지로 나타날 것이다.
    • receiverValue + argumentValue
    • selectorPointer bitShift: -1
  4. 4. 배열 접근. 인터프리터가 유지하는 특정 테이블은 Arrays에 의한 형식적 명세에 표현될 것이다. 이러한 테이블로의 접근은 Arrays에 대한 at:과 at:put: 메시지로 나타날 것이다.
    • methodCache at: hash
    • semaphoreList at: semaphoreIndex put: semaphorePointer
  5. 5. 조건부 제어 구조체. 가상 머신의 제어 구조체는 표준 Smalltalk 조건부 제어 구조체로 표현될 것이다. 조건부 선택은 Booleans에 대한 메시지로 나타날 것이다. 조건부 반복은 블록에 대한 메시지로 나타날 것이다.
    • index < length ifTrue: [ ... ]
    • sizeFlag = 1 ifTrue: [ ... ]
          ifFalse: [ ... ]
    • [currentClass ~= NilPointer] whileTrue: [ ... ]


Interpreter의 정의는 Smalltalk-80 바이트코드 인터프리터의 기능을 설명하지만 인터프리터의 기계어 구현 형태는 매우 상이할 수 있으며, 특히 그것이 사용하는 제어 구조체에서 더 그러하다. 바이트코드를 실행하기 위해 적절한 루틴으로의 전달(dispatch)은 기계어 인터프리터가 뭔가 다르게 실행할 수 있는 것을 보여주는 예제이다. 실행하기에 올바른 루틴을 찾기 위해서는 기계어 인터프리터가 어디로 건너뛸 것인지 계산하기 위해 일종의 어드레스 산술(address arithmetic)을 실행하는 반면 Interpreter는 일련의 조건문과 루틴 호출을 실행함을 확인할 수 있다. 기계어 구현에서 각 바이트코드를 실행하는 루틴들은 루틴 호출 구조체를 통해 리턴하는 대신 단순히 완료되면 바이트코드 fetch 루틴의 시작으로 건너뛴다.


Interpreter와 기계어 구현의 또 다른 차이는 코드의 최적화 수준이다. 본장에 명시된 루틴들은 최적화되지 않았음을 분명히 밝히는 바이다. 가령, Interpreter는 작업을 실행하기 위해 여러 루틴에서 객체 메모리로부터 여러 번 포인터를 인출하는 반면 좀 더 최적화된 인터프리터는 추후 사용을 위해 레지스터에 값을 저장할 수도 있다. 형식적 명세에서 많은 루틴들은 기계어 구현에서 하위루틴이 되지는 않겠지만 대신 일렬로(in-line)으로 작성될 것이다.


객체 메모리 인터페이스(Object Memory Interface)

제 26장에서는 객체 메모리를 간단하게 설명하였다. Interpreter의 루틴들은 객체 메모리와 상호작용할 필요가 있으므로 우리는 형식적인 기능적 명세가 필요하다. 이는 ObjectMemory 클래스의 프로토콜 명세로 제시할 것이다. 제 30장은 이러한 프로토콜 명세를 구현하는 한 가지 방법을 설명할 것이다.

  1. 클래스를 설명하는 객체의 객체 포인터와
  2. 객체 포인터 또는 수치값을 포함하는 8- 또는 16-bit 필드 집합.


객체 메모리에 대한 인터페이스는 객체의 필드를 나타내기 위해 zero-relative 정수 색인을 사용한다. Integer의 인스턴스는 인터프리터와 객체 메모리 간 인터페이스에서 객체 포인터와 필드 색인에 모두 사용된다.


ObjectMemory의 프로토콜은 객체의 필드에 객체 포인터 또는 수치값을 저장하고 꺼내기 위한 메시지 쌍을 포함한다.

객체 포인터 접근(object pointer access)
fetchPointer: fieldIndex ofObject: objectPointer objectPointer와 연관된 객체의 fieldIndex 숫자로 매겨진 필드에서 찾은 객체 포인터를 리턴한다.
storePointer: fieldIndex ofObject: objectPointer withValue: valuePointer objectPointer와 연관된 객체의 fieldIndex 숫자로 매겨진 필드에서 객체 포인터 valuePointer를 저장한다.
워드 접근(word access)
fetchWord: fieldIndex ofObject: objectPointer objectPointer와 연관된 객체의 fieldIndex 숫자로 매겨진 필드에서 찾은 16-bit 수치값을 리턴한다.
storeWord: fieldIndex ofObject: objectPointer withValue: valueWord objectPointer와 연관된 객체의 fieldIndex 숫자로 매겨진 필드에 16-bit 수치값 valueWord를 저장한다.
바이트 접근(byte access)
fetchByte: byteIndex ofObject: objectPointer objectPointer와 연관된 객체의 byteIndex 숫자로 매겨진 바이트에서 찾은 8-bit 수치값을 리턴한다.
storeByte: byteIndex ofObject: objectPointer withValue: valueByte objectPointer와 연관된 객체의 byteIndex 숫자로 매겨진 바이트에 8-bit 수치값 valueByte를 저장한다.


fetchPointer:ofObject: 과 fetchWord:ofObject: 모두 16-bit 양을 로드하기 때문에 동일한 방식으로 구현될 것이란 점을 주목한다. 하지만 storePointer:ofObject:의 구현은 객체 메모리가 동적인 참조 계수를 유지할 경우 참조 계수를 실행할 것이기 때문에 (30장 참조) storeWord:ofObject:의 구현과는 다를 것이다. 균형을 위해 fetchPointer:ofObject:과 fetchWord:ofObject:에 대해 구분된 인터페이스를 유지했다.


참조 계수의 유지는 대부분 storePointer:ofObject:withValue: 루틴에서 자동으로 이루어지지만 인터페이스 루틴이 참조 계수를 직접 조작해야만 하는 시점이 있다. 따라서 다음 두 개의 루틴이 객체 메모리 인터페이스에 포함된다. 객체 메모리가 참조되지 않은 객체를 회수하기 위해 쓰레기 수집만 사용할 경우 이러한 루틴은 no-ops이다.

reference counting
    increaseReferencesTo: objectPointer||Add one to the reference count of the object whose object pointer is objectPointer.
    decreaseReferencesTo: objectPointer||Subtract one from the reference count of the object whose object pointer is objectPointer.


모든 객체는 그 클래스 설명의 객체 포인터를 포함하므로 해당 포인터는 객체의 필드 중 하나의 내용으로 간주할 수 있다. 하지만 다른 필드와 달리 객체의 클래스가 인출될 수도 있으나 그 값은 변경되지 않는다. 포인터의 이러한 특징을 감안해 동일한 방식으로 접근하지 않기로 결정했다. 따라서 객체의 클래스를 인출하기 위한 특수 프로토콜이 있다.

class pointer access
    fetchClassOf: objectPointer||Return the object pointer of the class-describing object for the object associated with objectPointer.


객체의 길이 또한 그 필드 중 하나의 내용으로 간주할 수도 있다. 하지만 변경되지 않을지도 모른다는 점에서 클래스 필드와 같다. 객체 메모리 프로토콜에는 객체 내 워드의 개수와 객체 내 바이트의 개수를 요청하는 메시지가 하나씩 있다. 이번에는 워드와 포인터를 따로 구별하지 않았는데, 둘 다 정확히 하나의 필드에 들어맞기 때문이다.

length access
    fetchWordLengthOf: objectPointer||Return the number of fields in the object associated with objectPointer.
    fetchByteLengthOf: objectPointer||Return the number of byte fields in the object associated with objectPointer.


객체 메모리의 또 다른 중요한 서비스로 새로운 객체를 생성하는 것을 들 수 있다. 객체 메모리는 클래스와 길이와 함께 제공되어야 하고, 새로운 객체 포인터로 응답할 것이다. 다시 말하지만 포인터, 워드 또는 바이트로 객체를 생성하는 세 가지 버전이 있다.

객체 생성(object creation)
instantiateClass: classPointer withPointers: instanceSize 포인터를 포함하게 될 instanceSize 필드와 함께 classPointer를 객체 포인터로 갖는 클래스의 새로운 인스턴스를 생성한다. 새로운 객체의 객체 포인터를 리턴한다.
instantiateClass: classPointer withWords: instanceSize 16-bit 수치값을 포함하게 될 instanceSize 필드와 함께 classPointer를 객체 포인터로 갖는 클래스의 새로운 인스턴스를 생성한다. 새로운 객체의 객체 포인터를 리턴한다.
instantiateClass: classPointer withBytes: instanceByteSize instanceByteSize 8-bit 수치값의 공간과 함께 classPointer를 객체 포인터로 갖는 클래스의 새로운 인스턴스를 생성한다. 새로운 객체의 객체 포인터를 리턴한다.


객체 메모리의 두 가지 루틴은 클래스의 인스턴스를 열거할 수 있도록 한다. 이는 객체 포인터의 임시 정렬을 따른다. 포인터 자체의 번호순을 사용하는 것이 바람직하다.

인스턴스 열거(instance enumeration)
initialInstanceOf: classPointer 정의된 순서로 (예: 가장 작은 객체 포인터를 가진) classPointer를 객체 포인터로 가진 클래스의 첫 번째 인스턴스의 객체 포인터를 리턴한다.
instanceAfter: objectPointer 정의된 순서로 (예: 다음으로 큰 객체 포인터를 가진) objectPointer를 객체 포인터로 가진 객체와 동일한 클래스의 다음 인스턴스의 객체 포인터를 리턴한다.


객체 메모리의 또 다른 루틴은 두 객체의 객체 포인터를 서로 교환하도록 해준다.

포인터 스와핑(pointer swapping)
swapPointersOf: firstPointer and: secondPointer firstPointer는 secondPointer를 객체 포인터로 가진 객체를 참조하도록 하고, secondPointer는 firstPointer를 객체 포인터로 가진 객체를 참조하도록 한다.


제 26장에 설명된 바와 같이 -16384와 16384 사이의 정수는 1이 최하위 비트에 위치하고 적절한 2의 보수값이 최상위 15 비트에 저장된 객체 포인터로 직접 인코딩된다. 이러한 객체들은 SmallInteger 클래스의 인스턴스다. SmallInteger의 값은 보통 필드에 저장되는데, 사실상 그 객체 포인터로부터 결정된다. 따라서 인터프리터는 값을 SmallInteger의 필드에 저장하는 대신 바람직한 값으로 (integerObjectOf: 루틴을 사용하여) SmallInteger의 객체 포인터를 요청해야 한다. 그리고 필드로부터 값을 인출하는 대신 객체 포인터와 연관된 값을 요청해야 한다 (integerValueOf: 루틴을 사용하여). 객체 포인터가 SmallInteger를 사용하는지(isIntegerObject:)와 값이 SmallInteger로 표현된 올바른 범위에 위치하는지 (isIntegerValue:) 결정하는 루틴이 두 가지 있다. isIntegerObject: 루틴의 기능 또한 객체의 클래스를 요청하여 그것이 SmallInteger인지 확인함으로써 실행할 수 있다.

숫자형 접근(integer access)
integerValueOf: objectPointer objectPointer를 포인터로 가진 SmallInteger의 인스턴스 값을 리턴한다.
integerObjectOf: value value를 값으로 가진 SmallInteger의 인스턴스에 대한 객체 포인터를 리턴한다.
isIntegerObject: objectPointer objectPointer가 SmallInteger의 인스턴스일 경우 true를, 아닐 경우 false를 리턴한다.
isIntegerValue: value value를 SmallInteger의 인스턴스로 표현할 수 있을 경우 true를, 없을 경우 false를 리턴한다.


인터프리터는 SmallIntegers를 포함하는 필드로 접근하기 위한 특수 루틴을 두 가지 제공한다. fetchInteger:ofObject: 루틴은 포인터가 명시된 필드에 저장된 SmallInteger의 값을 리턴한다. 포인터가 SmallInteger용인지 확인하는 검사는 non-SmallInteger를 용인할 수 있을 때 이러한 루틴의 사용을 위해 만들어진다. primitiveFail 루틴은 프리미트 루틴에 관한 절에서 설명할 것이다.

fetchInteger: fieldIndex ofObject: objectPointer
    | integerPointer |
    integerPointer  memory fetchPointer: fieldIndex
            ofObject: objectPointer.
    (memory isIntegerObject: integerPointer)
        ifTrue: [memory integerValueOf: integerPointer]
        ifFalse: [self primitiveFail]


storeInteger:ofObject:withValue: 루틴은 명시된 값을 가진 SmallInteger의 포인터를 명시된 필드에 저장한다.

storeInteger: fieldIndex ofObject: objectPointer withValue: integerValue
    | integerPointer |
    (memory isIntegerValue: integerValue)
        ifTrue: [integerPointer  memory integerObjectOf: integerValue.
            memory storePointer: fieldIndex
                ofObject: objectPointer
                withValue: integerPointer]
        ifFalse: [self primitiveFail]


인터프리터는 여러 개의 포인터를 하나의 객체에서 다른 객체로 전달하는 루틴을 제공하기도 한다. 전달하기 위한 포인터 개수, 소스와 목적지 객체의 객체 포인터와 초기 필드 색인을 인자로 취한다.

transfer: count
        fromIndex: firstFrom
        ofObject: fromOop
        toIndex: firstTo
        ofObject: toOop
    | fromIndex toIndex lastFrom oop |
    fromIndex  first From.
    lastFrom  firstFrom + count.
    toIndex  firstTo.
    [fromIndex < lastFrom] whileTrue:
        [oop  memory fetchPointer: fromIndex
                        ofObject: fromOop.
            memory storePointer: toIndex
                ofObject: toOop
                withValue: oop
            memory storePointer: fromIndex
                ofObject: fromOop
                withValue: NilPointer.
            fromIndex  fromIndex + 1.
            toIndex  toIndex + 1]


인터프리터는 열거값으로부터 비트 필드를 추출하기 위한 루틴도 제공한다. 이러한 루틴들은 색인이 0인 최상위 비트와 색인이 15인 최하위 비트를 참조한다.

extractBits: firstBitIndex to: lastBitIndex of: anInteger
    (anInteger bitShift: lastBitIndex - 15)
        bitAnd: (2 raisedTo: lastBitIndex - firstBitIndex + 1) - 1
highByteOf: anInteger
    self extractBits: 0 to: 7
        of: anInteger
lowByteOf: anInteger
    self extractBits: 8 to: 15
        of: anInteger


인터프리터가 사용하는 객체(Objects Used by the Interpreter)

이번 절에서는 인터프리터의 데이터 구조체라고 불리는 것을 설명하고자 한다. 이들은 객체여서 데이터 구조체 이상이지만 인터프리터는 이러한 객체를 데이터 구조체로 취급한다. 첫 두 개의 객체 유형은 대부분 언어에서 인터프리터 내에서 발견되는 데이터 구조체에 해당한다. "Method"(메서드)는 프로그램, 하위루틴, 또는 프로시저에 해당한다. "컨텍스트"는 스택 프레임 또는 활성 레코드에 해당한다. 이번 절에서 마지막으로 설명하는 구조체는 "클래스"의 구조체로, 대부분 언어에서는 인터프리터에 사용되지 않고 컴파일러에 의해 사용된다. 클래스는 몇몇 다른 언어의 타입 선언 측면에 해당한다. Smalltalk 메시지의 특성 때문에 클래스는 인터프리터에 의해 런타임에서 사용되어야 한다.


공식 명세에는 많은 상수가 포함되어 있다. 대부분은 특정 객체 유형에 대한 필드 색인이나 알려진 객체의 객체 포인터를 나타낸다. 대부분의 상수는 명명될 것이며, 상수를 초기화하는 루틴은 그 값의 명세로서 포함될 것이다. 예를 들어, 아래의 루틴은 인터프리터에 알려진 객체 포인터를 초기화한다.

initializeSmallIntegers
    "SmallIntegers"
    MinusOnePointer  65535.
    zeroPointer  1.
    OnePointer  3.
    TwoPointer  5
initializeGuaranteedPointers
    "UndefinedObject and Booleans"
    NilPointer  2.
    FalsePointer  4.
    TruePointer  6.
    "Root"
    SchedulerAssociationPointer  8.
    "Classes"
    ClassStringPointer  14.
    ClassArrayPointer  16.
    ClassMethodContextPointer  22.
    ClassBlockContextPointer  24.
    ClassPointPointer  26.
    ClassLargePositiveIntegerPointer  28.
    ClassMessagePointer  32.
    ClassCharacterPointer  40.
    "Selectors"
    DoesNotUnderstandSelector  42.
    CannotReturnSelector  44.
    MustBeBooleanSelector  52.
    "Tables"
    SpecialSelectorsPointer  48.
    CharacterTablePointer  50


컴파일된 메서드(Compiled Methods)

인터프리터가 실행한 바이트코드는 CompiledMethod의 인스턴스에서 찾을 수 있다. 바이트코드는 8-bit 값으로 워드마다 2개의 값이 저장된다. 바이트코드 외에도 CompiledMethod는 몇 가지 객체 포인터를 포함한다. 이러한 객체 포인터 중 첫 번째는 "method header"(메서드 헤더)라고 부르고, 나머지 객체 포인터는 메서드의 "literal frame"(리터럴 프레임)을 구성한다. 그림 27.1은 CompiledMethod의 구조체를 표시하고, 다음의 루틴은 CompiledMethods의 필드로 접근하는 데 사용되는 색인을 초기화한다.

그림 27-1

initializeMethodIndices
    "Class CompiledMethod"
    HeaderIndex  0.
    LiteralStart  1


헤더는 CompiledMethod에 관한 특정 정보를 인코딩하는 SmallInteger이다.

headerOf: methodPointer
    memory fetchPointer: HeaderIndex
        ofObject: methodPointer


리터럴 프레임은 바이트코드에 의해 참조되는 객체에 대한 포인터를 포함한다. 메서드가 전송하는 메시지의 선택자, 공유 변수, 메서드가 참조하는 상수가 이에 포함된다.

literal: offset ofMethod: methodPointer
    memory fetchPointer: offset + LiteralStart
        ofObject: methodPointer


메서드의 리터럴과 헤더 다음에는 바이트코드가 따라온다. 메서드는 Smalltalk 시스템에서 (헤더와 리터럴 프레임에) 객체 포인터와 (바이트코드에) 수치값을 저장하는 유일한 객체다. 바이트코드의 형태는 다음 장에서 논하도록 하겠다.


❏ 메서드 헤더. 메서드 헤더는 SmallInteger이므로 그 값은 그 포인터에서 인코딩될 것이다. 포인터의 최상위 15 비트를 이용하여 정보를 인코딩할 수 있으며, SmallInteger에 대한 포인터를 나타내기 위한 최하위 비트는 1이어야 한다. 헤더는 CompiledMethod에 관한 정보를 인코딩하는 4 비트 필드를 포함한다. 그림 27.2는 헤더의 비트 필드를 표시한다.

그림 27-2


임시 계수는 CompiledMethod가 사용한 임시 변수의 개수를 나타낸다. 인자의 개수도 포함된다.

temporaryCountOf: methodPointer
    self extractBits: 3 to: 7
        of: (self headerOf: methodPointer)


MethodContext 두 개 크기를 나타내는 큰 컨텍스트 플래그가 필요하다. 플래그는 최대 스택 깊이와 필요로 하는 임시 변수의 개수의 합이 12보다 큰지 여부를 나타낸다. 작은 MethodContexts는 12의 공간을 갖고 있고 큰 MethodContexts는 32 공간을 갖고 있다.

largeContextFlagOf: methodPointer
    self extractBits: 8 to: 8
        of: (self headerOf: methodPointer)


리터럴 계수는 MethodContext의 리터럴 프레임 크기를 나타낸다. 그리고 이것은 MethodContext의 바이트코드가 시작되는 장소를 나타낸다.

literalCountOf: methodPointer
    self literalCountOfHeader: (self headerOf: methodPointer)
literalCountOfHeader: headerPointer
    self extractBits: 9 to: 14
        of: headerPointer


객체 포인터 계수는 헤더와 리터럴 프레임을 포함해 MethodContext 내에 객체 포인터의 총 개수를 나타낸다.

objectPointerCountOf: methodPointer
    (self literalCountOf: methodPointer) + LiteralStart


아래의 루틴은 CompiledMethod의 첫 번째 바이트코드의 바이트 색인을 리턴한다.

initialInstructionPointerOfMethod: methodPointer
    ((self literalCountOf: methodPointer) + LiteralStart) * 2 + 1


플래그 값은 CompiledMethod가 취하는 인자 개수와 그것이 연관된 프리미티브 루틴을 갖고 있는지 여부를 인코딩한다.

flagValueOf: methodPointer
    self extractBits: 0 to: 2
        of: (self headerOf: methodPointer)


8개의 가능한 플래그 값은 다음의 의미를 갖고 있다:

플래그 값 의미
0-4 프리미티브 없음, 0-4개 인자
5 self의 프리미티브 리턴 (인자 0개)
6 인스턴스 변수의 프리미티브 리턴 (인자 0개)
7 헤더 확장은 인자 개수와 프리미티브 색인을 포함한다


대다수의 CompiledMethods는 4개 또는 그 이하의 인자를 가지되 연관된 프리미티브 루틴을 갖지 않기 때문에 플래그값은 주로 인자의 개수가 된다.


❏ 특수 프리미티브 메서드. 메시지의 (self) 수신자를 리턴하기만 하는 Smalltalk 메서드는 리터럴 또는 바이트코드가 없이 플래그 값이 5인 헤더만 가진 CompiledMethods를 생성한다. 이와 비슷하게 수신자의 인스턴스 변수 중 하나의 값만 리턴하는 Smalltalk 메서드는 플래그 값이 6인 헤더만 포함한 CompiledMethods를 생성한다. 그 외 모든 메서드는 바이트코드가 있는 CompiledMethods를 생성한다. 플래그 값이 6이면 리턴해야 할 인스턴스 변수의 색인은 CompiledMethod가 사용한 임시 변수 개수를 나타내는 데 주로 사용되는 비트 필드 내 헤더에서 찾을 수 있다. 그림 27.3은 수신자 인스턴스 변수만 리턴하는 Smalltalk 메서드에 대한 CompiledMethod를 보여준다.

그림 27-3


다음 루틴은 플래그 값이 6일 때 리턴될 인스턴스 변수를 나타내는 필드의 색인을 리턴한다.

fieldIndexOf: methodPointer
    self extractBits: 3 to: 7
        of: (self headerOf: methodPointer)


❏ 메서드 헤더 확장. 플래그 값이 7일 경우 마지막 리터럴 옆에는 헤더 확장이 오는데, 이는 또 다른 SmallInteger에 해당한다. 헤더 확장은 CompiledMethod의 프리미티브 색인과 인자 계수를 인코딩하는 비트 필드 2개를 포함한다. 그림 27.4는 헤더 확장의 비트 필드를 표시한다.

그림 27-4


아래의 루틴은 헤더 확장과 그 비트 필드로 접근할때 사용된다.

headerExtensionOf: methodPointer
    | literalCount |
    literalCount  self literalCountOf: methodPointer.
    self literal: literalCount - 2
        ofMethod: methodPointer
argumentCountOf: methodPointer
    | flagValue |
    flagValue  self flagValueOf: methodPointer.
    flagValue < 5
        ifTrue: [flagValue].
    flagValue < 7
        ifTrue: [0]
        ifFalse: [self extractBits: 2 to: 6
            of: (self headerExtensionOf: methodPointer)]
primitiveIndexOf: methodPointer
    | flagValue |
    flagValue  self flagValueOf: methodPointer.
    flagValue = 7
        ifTrue: [self extractBits: 7 to 14
            of: (self headerExtensionOf: methodPointer)]
        ifFalse: [0]


슈퍼클래스로 메시지를 전송하거나 (예: super로 메시지) 헤더 확장을 포함하는 CompiledMethod는 CompiledMethod가 발견되는 메시지 사전을 가진 클래스를 값으로 둔 Association을 마지막 리터럴로 갖게 될 것이다. 이를 "method class"(메서드 클래스)라고 부르며, 다음 루틴에서 접근한다.

methodClassOf: methodPointer
    | literalCount association |
    literalCount  self literalCountOf: methodPointer.
    association  self literal: literalCount - 1
            ofMethod: methodPointer.
    memory fetchPointer: ValueIndex
        ofObject: association


리터럴 프레임이 메서드 클래스를 포함했던 CompiledMethod의 예제는 마지막 장에서 제공할 것이다. ShadedRectangle로 전송되는 intersect: 메시지에 대한 CompiledMethod는 마지막 장에 메시지와 관련된 절에서 표시되어 있다.


컨텍스트(Contexts)

인터프리터는 CompiledMethods와 블록의 실행 상태를 나타내기 위해 "contexts"(컨텍스트)를 사용한다. 컨텍스트는 MethodContext이거나 BlockContext가 될 수 있다. MethodContext는 메시지가 호출한 CompiledMethod의 실행을 나타낸다. 그림 27.5는 MethodContext와 그것의 CompiledMethod를 표시한다.

그림 27-5


BlockContext는 CompiledMethod에서 마주치는 블록을 나타낸다. BlockContext는 그것이 표현하는 블록을 포함한 CompiledMethod를 가진 MethodContext를 참조한다. 이를 BlockContext의 "home"이라고 부른다. 그림 27.6은 BlockContext와 그것의 home을 보여준다.


컨텍스트의 필드로 접근하는 데 사용된 색인은 다음 루틴으로 초기화된다.

initializeContextIndices
    "Class MethodContext"
    SenderIndex  0.
    InstructionPointerIndex  1.
    StackPointerIndex  2.
    MethodIndex  3.
    ReceiverIndex  5.
    TempFrameStart  6.
    "Class BlockContext"
    CallerIndex  0.
    BlockArgumentCountIndex  3.
    InitialIPIndex  4.
    HomeIndex  5


두 종류의 컨텍스트 모두 6개의 명명된 인스턴스 변수에 해당하는 6개의 고정 필드를 갖는다. 이러한 고정된 필드 다음에는 색인 가능한 필드 몇 개가 따라온다. 색인 가능한 필드는 임시 프레임(인자와 임시 변수), 그 다음에는 평가 스택의 내용을 저장하는 데 사용된다. 다음의 루틴은 컨텍스트에 저장된 스택 포인터와 명령 포인터의 인출 및 저장 시 사용된다.

그림 27-6


instructionPointerOfContext: contextPointer
    self fetchInteger: InstructionPointerIndex
        ofObject: contextPointer
storeInstructionsPointerValue: value inContext: contextPointer
    self storeInteger: InstructionPointerIndex
        ofObject: contextPointer
        withValue: value
stackPointerOfContext: contextPointer
    self fetchInteger: StackPointerIndex
        ofObject: contextPointer
storeStackPointerValue: value inContext: contextPointer
    self storeInteger: StackPointerIndex
        ofObject: contextPointer
        withValue: value


BlockContext는 그것이 기대하는 블록 인자의 개수를 필드 중 하나에 저장한다.

argumentCountOfBlock: blockPointer
    self fetchInteger: BlockArgumentCountIndex
        ofObject: blockPointer


최근에 실행 중인 블록 또는 CompiledMethod를 나타내는 컨텍스트를 "active context"(활성 컨텍스트)라고 부른다. 인터프리터는 가장 자주 사용하는 활성 컨텍스트 부분의 내용을 레지스터에 cache한다. 그러한 레지스터로는 다음이 있다.

인터프리터의 컨텍스트 관련 레지스터(Context-related Registers of the Interpreter)
activeContext 이 자체가 활성 컨텍스트다. MethodContext 또는 BlockContext에 해당한다.
homeContext 활성 컨텍스트가 MethodContext일 경우 home 컨텍스트는 동일한 컨텍스트가 된다. 활성 컨텍스트가 BlockContext일 경우 home 컨텍스트는 활성 컨텍스트의 home 필드의 내용이 된다. 이는 항상 MethodContext가 될 것이다.
method 인터프리터가 실행 중인 바이트코드를 포함하는 CompiledMethod이다.
receiver Home 컨텍스트의 메서드를 호출한 메시지를 수신한 객체이다.
instructionPointer 실행될 메서드의 다음 바이트코드의 바이트 색인이다.
stackPointer 스택의 맨 위를 포함하는 활성 컨텍스트의 필드에 대한 색인이다.


활성 컨텍스트가 변경될 때마다 (새로운 CompiledMethod가 호출될 때, CompiledMethod가 리턴할 때, 또는 프로세스 전환이 발생할 때) 이러한 레지스터는 모두 다음 루틴을 이용해 업데이트되어야 한다.

fetchContextRegisters
    (self isBlockContext: activeContext)
        ifTrue: [homeContext  memory fetchPointer: HomeIndex
                    ofObject: activeContext]
        ifFalse: [homeContext  activeContext].
    receiver  memory fetchPointer: ReceiverIndex
                ofObject: homeContext.
    method  memory fetchPointer: MethodIndex
                ofObject: homeContext.
    instructionPointer  (self instructionPointerOfContext: activeContext) - 1.
    stackPointer 
        (self stackPointerOfContext: activeContext) + TempFrameStart - 1


receiver와 method는 homeContext로부터 인출되고, instructionPointer와 stackPointer는 activeContext로부터 인출됨을 주목한다. 인터프리터는 MethodContexts와 BlockContexts의 차이, 즉 MethodContexts는 메서드 포인터를 (객체 포인터) 저장하고 BlockContexts는 같은 필드에 블록 인자의 (정수 포인터) 개수를 저장한다는 사실을 기반으로 둘을 구별한다. 이러한 위치에 정수 포인터가 포함된 경우 컨텍스트는 BlockContext가 되고, 그 외의 경우 MethodContext가 된다. 컨텍스트의 클래스를 기반으로 차이를 할 수도 있지만 MethodContext와 BlockContext의 서브클래스에 특별 규칙을 만들어야 할 것이다.

isBlockContext: contextPointer
    | methodOrArguments |
    methodOrArguments  memory fetchPointer: MethodIndex
        ofObject: contextPointer.
    memory isIntegerObject: methodOrArguments


새로운 컨텍스트가 활성 컨텍스트가 되기 전에 다음 루틴을 이용해 명령 포인터와 스택 포인터의 값이 활성 컨텍스트에 저장되어야 한다.

storeContextRegisters
    self storeInstructionPointerValue: instructionPointer + 1
        inContext: activeContext.
    self storeStackPointerValue: stackPointer - TempFrameStart + 1
        inContext: activeContext


그 외 cache된 레지스터의 값은 변경되지 않으므로 컨텍스트에 다시 저장될 필요가 없다. 컨텍스트에 저장된 명령 포인터는 메서드의 필드에 대한 one-relative 색인인데, Smalltalk에서 배열첨자(subscripting)는 (예: at: 메시지) one-relative 색인을 취하기 때문이다. 하지만 메모리는 zero-relative 색인을 사용하므로 fetchContextRegisters 루틴은 1을 제하여 메모리 색인으로 변환하고 storeContextRegisters 루틴은 다시 1을 더한다. 컨텍스트에 저장된 스택 포인터는 평가 스택의 맨 위가 컨텍스트의 고정 필드에서 얼마나 벗어났는지를 알려주는데 (예: 임시 프레임의 시작부터 얼마나 먼지) Smalltalk에서 배열첨자는 고정된 필드를 고려하고 그 이후의 색인 가능 필드로부터 인출하기 때문이다. 하지만 메모리는 객체의 시작을 기준으로 한 색인을 원하므로 fetchContextRegisters는 임시 프레임의 시작 오프셋을 (상수) 더하고 storeContextRegisters 루틴은 오프셋을 제한다.


아래 루틴은 활성 컨텍스트의 스택에 다양한 연산을 실행한다.

push: object
    stackPointer  stackPointer + 1.
    memory storePointer: stackPointer
        ofObject: activeContext
        withValue: object
popStack
    | stackTop |
    stackTop  memory fetchPointer: stackPointer
    ofObject: activeContext.
    stackPointer  stackPointer - 1.
    stackTop
stackTop
    memory fetchPointer: stackPointer
        ofObject: activeContext
stackValue: offset
    memory fetchPointer: stackPointer-offset
        ofObject: activeContext
pop: number
    stackPointer  stackPointer - number
unPop: number
    stackPointer  stackPointer + number


활성 컨텍스트 레지스터는 참조되지 않은 객체를 할당해제하는 객체 메모리 부분에 대한 참조로 세야 한다. 객체 메모리가 동적 참조 계수를 유지할 경우, 활성 컨텍스트를 변경하기 위한 루틴은 적절한 참조 계수를 실행해야 한다.

newActiveContext: aContext
    self storeContextRegisters.
    memory decreaseReferencesTo: activeContext.
    activeContext  aContext.
    memory increaseReferencesTo: activeContext.
    self fetchContextRegisters


아래의 루틴은 인터프리터가 레지스터에 cache되지 않을 정도로 드물게 필요로 하는 컨텍스트의 필드를 인출한다. 전송자는 CompiledMethod가 값을 리턴할 때 ("1" 때문에 혹은 메서드의 끝에서) 리턴되어야 할 컨텍스트이다. 블록 내에서 명시적 리턴은 블록을 감싸는 CompiledMethod로부터 리턴해야 하므로 전송자는 home 컨텍스트로부터 인출된다.

sender
    memory fetchPointer: SenderIndex
        ofObject: homeContext


호출자는 (블록의 끝에서) BlockContext가 값을 리턴할 때 리턴되어야 할 컨텍스트이다.

caller
    memory fetchPointer: SenderIndex
        ofObject: activeContext


블록에서 참조되는 임시변수는 블록을 감싸는 CompiledMethod에서 참조된 것과 동일하기 때문에 임시변수는 home 컨텍스트로부터 인출된다.

temporary: offset
    memory fetchPointer: offset + TempFrameStart
        ofObject: homeContext


다음 루틴은 현재 실행 중인 CompiledMethod의 리터럴로 간편하게 접근하도록 해준다.

literal: offset
    self literal: offset
        ofMethod: method


클래스(classes)

인터프리터는 "message dictionary"(메시지 사전)를 검색함으로써 메시지에 대한 응답에서 실행하기에 적절한 CompiledMethod를 찾는다. 메시지 사전은 메시지 수신자의 "클래스" 또는 해당 클래스의 "슈퍼클래스" 중 하나에서 찾을 수 있다. 클래스와 그에 연관된 메시지 사전의 구조체는 그림 27.7에 표시된다. 메시지 사전과 슈퍼클래스 외에도 인터프리터는 그 인스턴스의 메모리 요구사항을 결정하기 위해 교실의 인스턴스 명세를 사용한다. 클래스의 다른 필드들은 Smalltalk 메서드에 의해서만 사용되고 인터프리터에게는 무시받는다. 다음 루틴은 클래스의 필드와 그들의 메시지 사전으로 접근하는 데 사용된 색인을 초기화한다.

그림 27-7


initializeClassIndices
    "Class Class"
    SuperclassIndex  0.
    MessageDictionaryIndex  1.
    InstanceSpecificationIndex  2.
    "Fields of a message dictionary"
    MethodArrayIndex  1.
    SelectorStart  2


인터프리터는 메시지 검색 과정의 상태를 cache하기 위해 몇 가지 레지스터를 사용한다.

클래스와 연관된 인터프리터의 레지스터(Class-related Registers of the interpreter)
messageSelector 전송되는 메시지의 선택자이다. 항상 Symbol에 해당한다.
argumentCount 현재 전송 중인 메시지에서 인자 개수에 해당한다. 인자 아래에 있으므로 스택에서 메시지 수신자가 발견되는 곳을 나타낸다.
newMethod messageSelector와 연관된 메서드이다.
primitiveIndex newMethod와 연관된 프리미티브 루틴이 있을 경우 그것의 색인에 해당한다.


메시지 사전은 IdentityDictionary이다. IdentifyDictionary는 Set의 내용과 연관된 값을 포함하는 추가 Array가 있는 Set의 서브클래스에 해당한다. 메시지 선택자는 Set에서 상속된 색인된 인스턴스 변수에 저장된다. CompiledMethods는 IdentityDictionary가 추가한 Array에 저장된다.CompiledMethod는 사전 객체 자체의 색인 가능한 변수에 선택자를 갖고 있는 해당 Array에서 동일한 이름을 갖고 있다. 선택자와 CompiledMethod를 저장할 색인은 해시 함수에 의해 계산된다.


선택자는 Symbol의 인스턴스이므로 그것의 상등성 검사는 그 객체 포인터를 대상으로 상등성을 검사함으로써 해결될 수 있겠다. Symbols의 객체 포인터는 상등성을 결정하므로 해시 함수는 객체 포인터의 함수일 수 있다. 객체 포인터는 반 무작위(quasi-randomly)하게 할당되므로 객체 포인터 자체가 적합한 해시 함수이다. 오른쪽으로 1 비트 전환된 포인터가 더 나은 해시 함수를 제공할 것인데, SmallIntegers를 제외한 모든 객체 포인터는 짝수이기 때문이다.

hash: objectPointer
    objectPointer bitShift: -1


메시지 선택자 검색에서는 동일한 해싱 함수를 이용해 메서드가 사전에 들어간 것으로 가정한다. 해싱 알고리즘은 사전 내 색인 가능한 위치의 개수를 모듈로 하여(modulo) 원래 해시 함수를 축소시킨다. 이는 사전 내에 색인을 제공한다. 모듈로 축소의 계산을 간단히 만들기 위해 메시지 사전은 두 개의 필드에 대해 정확한 권한(exact power)을 갖고 있다. 따라서 모듈로 계산은 적절한 비트 개수를 masking off하여 실행할 수 있다. 초기 해시 위치에서 선택자가 발견되지 않으면 선택자를 찾거나 nil에 마주칠 때까지 연속적 필드가 검사된다. 검색에서 nil에 마주치면 선택자는 사전에 위치하지 않은 것이다. 검색 중에 사전의 끝에 마주치면 검색은 랩 어라운드하여 첫 번째 필드로 계속한다.


다음의 루틴은 messageSelector 레지스터 내 Symbol과 연관된 CompiledMethod를 사전에서 검색한다. Symbol을 찾으면 연관된 CompiledMethod의 포인터를 newMethod 레지스터로 저장하고, 그 프리미티브 색인을 primitiveIndex 레지스터로 저장한 후 true를 리턴한다. Symbol을 사전에서 찾을 수 없다면 루틴은 false를 리턴한다. nil 또는 적절한 Symbol을 발견하는 것은 루프의 유일한 exit conditions이므로 루틴은 전체 사전에서 검사되어야 한다 (예: nils 없이). 이는 랩 어라운드의 발생 여부를 추적하여 이루어진다. 검색이 두 번의 랩 어라운드를 실행했다면 선택자는 사전에 존재하지 않는 것이다.

lookupMethodInDictionary: dictionary
    | length index mask wrapAround nextSelector methodArray |
    length  memory fetchWordLengthOf: dictionary.
    mask  length - SelectorStart - 1.
    index  (mask bitAnd: (self hash: messageSelector)) + SelectorStart.
    wrapAround  false.
    [true] whileTrue:
        [nextSelector  memory fetchPointer: index
                                ofObject: dictionary.
            nextSelector = NilPointer ifTrue: [false].
            nextSelector = messageSelector
                ifTrue: [methodArray  memory fetchPointer: MethodArrayIndex
                                                ofObject: dictionary.
                            newMethod  memory fetchPointer: index - SelectorStart
                                                ofObject: methodArray.
                            primitiveIndex  self primitiveIndexOf: newMethod.
                            true].
            index  index + 1.
            index = length
                ifTrue: [wrapAround ifTrue: [false].
                            wrapAround  true.
                            index  SelectorStart]]


이러한 루틴은 아래 루틴에서 클래스가 선택자와 연관시키는 메서드를 찾을 때 사용된다. 선택자가 초기 클래스의 사전에서 발견되지 않으면 슈퍼클래스 사슬의 다음 클래스에서 검색된다. 검색은 메서드가 발견되거나 슈퍼클래스 사슬이 모두 소진될 때까지 윗방향으로 계속된다.

lookupMethodInClass: class
    | currentClass dictionary |
    currentClass  class.
    [currentClass~=NilPointer] whileTrue:
        [dictionary  memory fetchPointer: MessageDictionaryIndex
                                ofObject: currentClass.
            (self lookupMethodInDictionary: dictionary)
                ifTrue: [true].
            currentClass  self superclassOf: currentClass].
    messageSelector = DoesNotUnderstandSelector
        ifTrue: [self error: 'Recursive not understood error encountered'].
    self createActualMessage.
    messageSelector  DoesNotUnderstandSelector.
    self lookupMethodInClass: class
superclassOf: classPointer
    memory fetchPointer: SuperclassIndex
        ofObject: classPointer


클래스와 슈퍼클래스가 메시지 선택자와 연관된 CompiledMethod를 포함하지 않은 객체로 메시지가 전송되면 인터프리터는 색다른 조치를 취해야 한다. Smalltalk 철학에 맞춰 인터프리터는 메시지를 전송한다. 이러한 메시지에 대한 CompiledMethod는 발견되도록 보장되어 있다. 인터프리터는 원본 메시지를 Message 클래스의 인스턴스에 싼 후에 doesNotUnderstand: 선택자와 연관된 CompiledMethod를 검색한다. 메시지는 doesNotUnderstand: 메시지에 대한 유일한 인자가 된다. doesNotUnderstand: 메시지는 사용자에게 알리는 CompiledMethod와 함께 Object에서 정의된다. 이러한 CompiledMethod는 사용자 정의 클래스에서 다른 일을 수행하도록 오버라이드할 수 있다. 이러한 이유로 lookupMethodInClass: 루틴은 항상 newMethod 레지스터에 CompiledMethod에 대한 포인터를 저장함으로써 완료할 것이다.

createActualMessage
    | argumentArray message |
    argumentArray  memory instantiateClass: ClassArrayPointer
            withPointers: argumentCount.
    message  memory instantiateClass: ClassMessagePointer
            withPointers: self messageSize.
    memory storePointer: MessageSelectorIndex
        ofObject: message
        withValue: messageSelector.
    memory storePointer: MessageArgumentsIndex
        ofObject: message
        withValue: argumentArray.
    self transfer: argumentCount
        fromField: stackPointer - (argumentCount - 1)
        ofObject: activeContext
        toField: 0
        ofObject: argumentArray.
    self pop: argumentCount.
    self push: message.
    argumentCount < -


다음 루틴은 Message의 필드로 접근하는 데 사용되는 색인을 초기화한다.

initializeMessageIndices
    MessageSelectorIndex  0.
    MessageArgumentsIndex  1.
    MessageSize  2


클래스의 인스턴스 명세 필드는 다음의 네 가지 정보를 인코딩하는 SmallInteger 포인터를 포함한다.

  1. 인스턴스의 필드가 객체 포인터 또는 수치값을 포함하는지 여부
  2. 인스턴스의 필드가 워드 또는 바이트 양에 어드레스 되었는지 여부
  3. 인스턴스가 고정된 필드를 벗어난 색인 가능 필드를 갖는지 여부
  4. 인스턴스가 갖고 있는 고정 필드의 개수


그림 27.8은 인스턴스 명세에 이러한 정보가 어떻게 인코딩되는지를 보여준다.

그림 27-8


네 가지 정보는 서로 독립적인 정보가 아니다. 인스턴스의 필드가 객체 포인터를 포함할 경우 이는 워드 양으로 어드레스될 것이다. 인스턴스의 필드가 수치값을 포함한다면 색인 가능한 필드를 갖게 되고 고정된 필드는 갖지 않게 될 것이다.

instanceSpecificationOf: classPointer
    memory fetchPointer: InstanceSpecificationIndex
        ofObject: classPointer
isPointers: classPointer
    | pointersFlag |
    pointersFlag  self extractBits: 0 to: 0
        of: (self instanceSpecificationOf: classPointer).
    pointersFlag = 1
isWords: classPointer
    | wordsFlag |
    wordsFlag  self extractBits: 1 to: 1
        of: (self instanceSpecificationOf: classPointer).
    wordsFlag = 1
isIndexable: classPointer
    | indexableFlag |
    indexableFlag  self extractBits: 2 to: 2
        of: (self instanceSpecificationOf: classPointer).
    indexableFlag = 1
fixedFieldsOf: classPointer
    self extractBits: 4 to: 14
        of: (self instanceSpecificationOf: classPointer)


주의: CompiledMethod의 인스턴스 명세는 그 인스턴스의 구조체를 정확하게 반영하지 않는데, CompiledMethods가 동질적이지 않기 때문이다. 인스턴스 명세는 인스턴스가 포인터를 포함하지 않고 바이트에 의해 어드레스되었다고 말한다. 이는 CompiledMethod의 바이트코드 부분에서만 사실에 해당한다. 저장공간 관리자는 CompiledMethods가 특별하며 사실상 일부 포인터를 포함하고 있음을 알아야 한다. 그 외 모든 클래스의 경우 인스턴스 명세는 정확하다.


Notes