1000sj
SJ CODE
1000sj
전체 방문자
오늘
어제
  • 분류 전체보기
    • Algorithms
      • Crypto
      • Formal Methods
    • Security
      • Fuzzing
      • Exploit
    • System Programming
      • Kernel
      • Compiler
      • Device Driver
      • Emulator
      • Assembly
      • Memory
      • Network
      • Hypervisor
    • Architecture
      • ARM
      • RISC-V
    • Cloud Computing
      • Infrastructure
      • SDN
    • TroubleShooting
      • Debugging
      • Testing
    • Performance improvements
      • Parrelel Processing
      • HPC
    • ETC
      • iamroot 스터디
      • 문화 생활
      • 커뮤니티

인기 글

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
1000sj

SJ CODE

System Programming/Hypervisor

QEMU 코드 분석 #4 Machine & i440fx

2024. 9. 19. 09:20

1) i440fx 칩셋과 에뮬레이션 메인보드 아키텍처 (qtree)

2) 가상머신 초기화 (DEFINE_I440FX_MACHINE)

3) Machine 메커니즘 큰 그림 (type_init ~ type_register)

4) select_machine → MachineState 생성 (상세 추적)

5) 함수 추적 — 다른 시각의 분석

 

1) i440fx 칩셋과 에뮬레이션 메인보드 아키텍처

Intel 440FX는 Intel이 1996년에 발표한 Pentium II를 지원하기 위한 메인보드 칩으로, 지금까지 이미 20여년의 역사를 가졌으며, 한 세대를 풍미한 비교적 고전적인 아키텍처다. 비록 QEMU가 이미 더 진보한 q35 아키텍처의 에뮬레이션을 지원할 수 있지만, 현재 QEMU는 여전히 i440fx 아키텍처를 기본으로 사용한다.

 

당시, Intel사는 TRITON 시리즈 칩셋에 의지해서 막대한 이윤을 벌어들였지만 조금도 긴장을 늦추지 않았는데, 왜냐하면 그들은 PENTIUM PRO에 맞춰 더 새로운 칩셋을 생산해 낼 필요가 있었기 때문이다.

이렇게 440FX 칩셋이 탄생했다. 이 칩셋은 Intel이 생산한 SLOT 1 아키텍처 기반의 첫 번째 칩셋으로, NATOMA라고도 불린다. NATOMA는 네 개의 칩으로 구성된다: 82441FX/82442FX/82371SB/82093AA. 그 중 82093AA는 선택사항이며, 164-PIN PQFP 형식 패키지를 사용하고, 나머지 세 개는 3208-PIN PQFP 형식 패키지를 사용한다.

82093AA 이 칩의 역할은 440FX 칩셋이 두 개의 CPU를 지원하게 만들고 대칭 다중 프로세스 독립 I/O APIC 처리도 지원하는 데 있다.

NATOMA는 CACHE 범위 제한 문제가 존재하지 않는데, 왜냐하면 PENTIUM PRO CPU의 칩 내 2급 캐시는 모든 메인 메모리에 대해 CACHE 조작을 진행할 수 있기 때문이다.

메모리 관리 측면에서, NATOMA는 매우 큰 진보가 있고, 그것은 1GB 메모리 용량을 지원할 수 있다. 그러나 또 한 가지 유감인 점은, 그것이 SDRAM에 대한 지원이 부족하다는 것이다. 이 외에 440FX는 PIIX3 하드디스크 컨트롤러를 사용하고, 버스 제어를 지원하며, 최대 데이터 전송률은 PIO MODE 4/DMA MODE 2(16.6MB/S)이고, 또한 USB도 지원하며, 동기 PCI를 지원하고, 그것이 지원하는 버스 속도는 60/66MHZ다.

 

Intel 440FX는 한 쌍의 칩으로 구성된 노스브리지로 구성된다. 주요 부품은 82441FX PCI 브리지와 메모리 컨트롤러, 그리고 PCI 버스용 82442FX 데이터 버스 가속기다.

해당 칩셋은 PIIX3 82371SB 사우스브리지 칩을 사용하며, 고속 버스 마스터 DMA IDE 인터페이스와 USB를 지원하고, PCI와 ISA 버스 사이의 가교 역할을 한다.

i440fx를 노스브리지로, piix3를 사우스브리지로 하는 칩셋 구조는 아래 그림과 같다.

위 그림이 보여주는 것처럼, i440fx 노스브리지는 PMC(PCI Bridge and Memory Controller) 및 DBX(Data Bus Accelerator)를 포함한다.

노스브리지의 역할은 위로는 여러 프로세서에 연결하고, 아래로는 메모리 및 PCI 버스에 연결하는 것이며, 해당 PCI 버스는 하나의 PCI 디바이스 트리를 파생시킬 수 있다. 노스브리지 안에 두어진 주요 부품은 기억장치 컨트롤러, 그래픽카드 컨트롤러와 Host 메인 브리지뿐이다.

pii3 사우스브리지는 주로 저속 장치를 연결하는 데 사용되는데, IDE 컨트롤러, USB 컨트롤러 등을 포함한다. 각종 느린 장치들은 컨트롤러를 통해 시스템에 연결될 수 있다. 예컨대 하드디스크, USB 장치 등(컨트롤러와 대응한다). 사우스브리지는 또 ISA 버스에 연결되어, 전통적인 ISA 장치가 이를 빌려 시스템에 연결될 수 있다.

여기서 또 한 가지 주의가 필요한 곳이 있는데, 즉 인터럽트 컨트롤러 I/O APIC(Advanced Programmable Interrupt Controller, 고급 가능 프로그램 인터럽트 컨트롤러)는 직접 프로세서에 연결되어, 장치의 인터럽트가 I/O APIC 경로를 통해 프로세서에 도달할 수 있다.

 

440FX 칩셋 구성 다이어그램

이 칩셋은 PIIX3 82371SB 사우스브리지 칩을 사용하고, 고속 버스 마스터 DMA IDE 인터페이스와 USB를 지원하며, PCI와 ISA 버스 사이의 다리 역할을 한다.

 

QEMU 에뮬레이션 메인보드 아키텍처

Intel 440FX 아키텍처와 비교적 가까우며, 둘의 기본 아키텍처는 일치한다.

 

QEMU monitor로 가상머신 디바이스 구조 확인

다음 명령으로 QEMU monitor에 진입한다.

qemu-system-x86_64 -vnc :1 -monitor stdio
또는
qemu -vnc :1 -monitor stdio


실제 명령 및 결과는 다음과 같다.

$ qemu-system-x86_64 -vnc :1 -monitor stdio
QEMU 7.1.0 monitor - type 'help' for more information
(qemu)

QEMU monitor에 "info qtree"를 입력하면, QEMU 가상머신의 디바이스 구조를 볼 수 있다. 다음과 같다.

$ qemu-system-x86_64 -vnc :1 -monitor stdio
QEMU 7.1.0 monitor - type 'help' for more information
(qemu) info qtree
bus: main-system-bus
  type System
  dev: ps2-mouse, id ""
    gpio-out "" 1
  dev: ps2-kbd, id ""
    gpio-out "" 1
  dev: hpet, id ""
    gpio-in "" 2
    gpio-out "" 1
    gpio-out "sysbus-irq" 32
    timers = 3 (0x3)
    msi = false
    hpet-intcap = 4 (0x4)
    hpet-offset-saved = true
    mmio 00000000fed00000/0000000000000400
  dev: ioapic, id ""
    gpio-in "" 24
    version = 32 (0x20)
    mmio 00000000fec00000/0000000000001000
  dev: i440FX-pcihost, id ""
    pci-hole64-size = 2147483648 (2 GiB)
    short_root_bus = 0 (0x0)
    x-pci-hole64-fix = true
    x-config-reg-migration-enabled = true
    bypass-iommu = false
    bus: pci.0
      type PCI
      dev: PIIX4_PM, id ""
        gpio-out "smi-irq" 1
        gpio-out "" 1
        smb_io_base = 1792 (0x700)
        disable_s3 = 0 (0x0)
        disable_s4 = 0 (0x0)
        s4_val = 2 (0x2)
        acpi-pci-hotplug-with-bridge-support = true
        acpi-root-pci-hotplug = true
        memory-hotplug-support = true
        smm-compat = false
        smm-enabled = true
        x-not-migrate-acpi-index = false
        addr = 01.3
        romfile = ""
        romsize = 4294967295 (0xffffffff)
        rombar = 1 (0x1)
        multifunction = false
        x-pcie-lnksta-dllla = true
        x-pcie-extcap-init = true
        failover_pair_id = ""
        acpi-index = 0 (0x0)
        class Bridge, addr 00:01.3, pci id 8086:7113 (sub 1af4:1100)
        bus: i2c
          type i2c-bus
          dev: smbus-eeprom, id ""
            address = 87 (0x57)
          dev: smbus-eeprom, id ""
            address = 86 (0x56)
          dev: smbus-eeprom, id ""
            address = 85 (0x55)
          dev: smbus-eeprom, id ""
            address = 84 (0x54)
          dev: smbus-eeprom, id ""
            address = 83 (0x53)
          dev: smbus-eeprom, id ""
            address = 82 (0x52)
          dev: smbus-eeprom, id ""
            address = 81 (0x51)
          dev: smbus-eeprom, id ""
            address = 80 (0x50)
      dev: piix3-ide, id ""
        addr = 01.1
        romfile = ""
        romsize = 4294967295 (0xffffffff)
        rombar = 1 (0x1)
        multifunction = false
        x-pcie-lnksta-dllla = true
        x-pcie-extcap-init = true
        failover_pair_id = ""
        acpi-index = 0 (0x0)
        class IDE controller, addr 00:01.1, pci id 8086:7010 (sub 1af4:1100)
        bar 4: i/o at 0xc040 [0xc04f]
        bus: ide.1
          type IDE
          dev: ide-cd, id ""
            drive = "ide1-cd0"
            backend_defaults = "auto"
            logical_block_size = 512 (512 B)
            physical_block_size = 512 (512 B)
            min_io_size = 0 (0 B)
            opt_io_size = 0 (0 B)
            discard_granularity = 512 (512 B)
            write-cache = "auto"
            share-rw = false
            rerror = "auto"
            werror = "auto"
            ver = "2.5+"
            wwn = 0 (0x0)
            serial = "QM00003"
            model = ""
            unit = 0 (0x0)
        bus: ide.0
          type IDE
      dev: e1000, id ""
        mac = "52:54:00:12:34:56"
        netdev = "hub0port0"
        autonegotiation = true
        mitigation = true
        extra_mac_registers = true
        migrate_tso_props = true
        init-vet = true
        addr = 03.0
        romfile = "efi-e1000.rom"
        romsize = 262144 (0x40000)
        rombar = 1 (0x1)
        multifunction = false
        x-pcie-lnksta-dllla = true
        x-pcie-extcap-init = true
        failover_pair_id = ""
        acpi-index = 0 (0x0)
        class Ethernet controller, addr 00:03.0, pci id 8086:100e (sub 1af4:1100)
        bar 0: mem at 0xfebc0000 [0xfebdffff]
        bar 1: i/o at 0xc000 [0xc03f]
        bar 6: mem at 0xffffffffffffffff [0x3fffe]
      dev: VGA, id ""
        vgamem_mb = 16 (0x10)
        mmio = true
        qemu-extended-regs = true
        edid = true
        xres = 1280 (0x500)
        yres = 800 (0x320)
        xmax = 0 (0x0)
        ymax = 0 (0x0)
        refresh_rate = 0 (0x0)
        global-vmstate = false
        addr = 02.0
        romfile = "vgabios-stdvga.bin"
        romsize = 65536 (0x10000)
        rombar = 1 (0x1)
        multifunction = false
        x-pcie-lnksta-dllla = true
        x-pcie-extcap-init = true
        failover_pair_id = ""
        acpi-index = 0 (0x0)
        class VGA controller, addr 00:02.0, pci id 1234:1111 (sub 1af4:1100)
        bar 0: mem at 0xfd000000 [0xfdffffff]
        bar 2: mem at 0xfebf0000 [0xfebf0fff]
        bar 6: mem at 0xffffffffffffffff [0xfffe]
      dev: PIIX3, id ""
        addr = 01.0
        romfile = ""
        romsize = 4294967295 (0xffffffff)
        rombar = 1 (0x1)
        multifunction = true
        x-pcie-lnksta-dllla = true
        x-pcie-extcap-init = true
        failover_pair_id = ""
        acpi-index = 0 (0x0)
        class ISA bridge, addr 00:01.0, pci id 8086:7000 (sub 1af4:1100)
        bus: isa.0
          type ISA
          dev: port92, id ""
            gpio-out "a20" 1
          dev: vmmouse, id ""
          dev: vmport, id ""
            x-read-set-eax = true
            x-signal-unsupported-cmd = true
            x-report-vmx-type = true
            x-cmds-v2 = true
            vmware-vmx-version = 6 (0x6)
            vmware-vmx-type = 2 (0x2)
          dev: i8042, id ""
            gpio-in "ps2-mouse-input-irq" 1
            gpio-in "ps2-kbd-input-irq" 1
            gpio-out "" 2
            gpio-out "a20" 1
            extended-state = true
            kbd-throttle = false
            kbd-irq = 1 (0x1)
            mouse-irq = 12 (0xc)
          dev: isa-fdc, id ""
            iobase = 1008 (0x3f0)
            irq = 6 (0x6)
            dma = 2 (0x2)
            fdtypeA = "auto"
            fdtypeB = "auto"
            fallback = "288"
            bus: floppy-bus.0
              type floppy-bus
              dev: floppy, id ""
                unit = 0 (0x0)
                drive = "floppy0"
                backend_defaults = "auto"
                logical_block_size = 512 (512 B)
                physical_block_size = 512 (512 B)
                min_io_size = 0 (0 B)
                opt_io_size = 0 (0 B)
                discard_granularity = 4294967295 (4 GiB)
                write-cache = "auto"
                share-rw = false
                drive-type = "288"
          dev: isa-parallel, id ""
            index = 0 (0x0)
            iobase = 888 (0x378)
            irq = 7 (0x7)
            chardev = "parallel0"
          dev: isa-serial, id ""
            index = 0 (0x0)
            iobase = 1016 (0x3f8)
            irq = 4 (0x4)
          dev: i8257, id ""
            base = 192 (0xc0)
            page-base = 136 (0x88)
            pageh-base = -1 (0xffffffffffffffff)
            dshift = 1 (0x1)
          dev: i8257, id ""
            base = 0 (0x0)
            page-base = 128 (0x80)
            pageh-base = -1 (0xffffffffffffffff)
            dshift = 0 (0x0)
          dev: isa-pcspk, id ""
            audiodev = ""
            iobase = 97 (0x61)
            migrate = true
          dev: isa-pit, id ""
            gpio-in "" 1
            gpio-out "" 1
            iobase = 64 (0x40)
          dev: mc146818rtc, id ""
            gpio-out "" 1
            base_year = 0 (0x0)
            iobase = 112 (0x70)
            irq = 8 (0x8)
            lost_tick_policy = "discard"
          dev: isa-i8259, id ""
            gpio-in "" 8
            gpio-out "" 1
            iobase = 160 (0xa0)
            elcr_addr = 1233 (0x4d1)
            elcr_mask = 222 (0xde)
            master = false
          dev: isa-i8259, id ""
            gpio-in "" 8
            gpio-out "" 1
            iobase = 32 (0x20)
            elcr_addr = 1232 (0x4d0)
            elcr_mask = 248 (0xf8)
            master = true
      dev: i440FX, id ""
        addr = 00.0
        romfile = ""
        romsize = 4294967295 (0xffffffff)
        rombar = 1 (0x1)
        multifunction = false
        x-pcie-lnksta-dllla = true
        x-pcie-extcap-init = true
        failover_pair_id = ""
        acpi-index = 0 (0x0)
        class Host bridge, addr 00:00.0, pci id 8086:1237 (sub 1af4:1100)
  dev: fw_cfg_io, id ""
    dma_enabled = true
    x-file-slots = 32 (0x20)
    acpi-mr-restore = true
  dev: kvmvapic, id ""
(qemu)

 

qtree 구조

QEMU는 가상머신의 모든 디바이스를 트리 구조로 관리한다(앞서 본 QOM 객체 트리, MemoryRegion 트리처럼).

이 디바이스 트리를 info qtree 명령으로 출력하면, 맨 위(루트)에 main-system-bus 라는 가상의 시스템 버스가 있고, 그 버스에 여러 디바이스가 가지처럼 연결돼 있다.

bus: main-system-bus
  type System
  dev: ps2-mouse, id ""
    gpio-out "" 1
  dev: ps2-kbd, id ""
    gpio-out "" 1
  dev: hpet, id ""
    gpio-in "" 2
    gpio-out "" 1
    gpio-out "sysbus-irq" 32
    timers = 3 (0x3)
    msi = false
    hpet-intcap = 4 (0x4)
    hpet-offset-saved = true
    mmio 00000000fed00000/0000000000000400
  dev: ioapic, id ""
    gpio-in "" 24
    version = 32 (0x20)
    mmio 00000000fec00000/0000000000001000
  dev: i440FX-pcihost, id ""
    pci-hole64-size = 2147483648 (2 GiB)
    short_root_bus = 0 (0x0)
    x-pci-hole64-fix = true
    x-config-reg-migration-enabled = true
    bypass-iommu = false
    bus: pci.0

 

당연히 가장 중요한 것은 노스브리지인 i440FX-pcihost인데, 이것은 main-system-bus라는 시스템 버스를 통해 CPU에 연결된다.

노스브리지 아래쪽은 하나의 PCI 루트 버스 pci.0 에 연결된다.

수많은 디바이스가 모두 이 pci.0 버스에 매달려 있다. 예를 들어 PIIX4_PM 디바이스는 전원 관리용이고, piix3-ide 디바이스는 IDE 컨트롤러인데, 그 아래에 다시 IDE 버스를 매달 수 있고, IDE 버스 아래에는 또 하드디스크 같은 IDE 디바이스를 매달 수 있다.

main-system-bus (시스템 버스, 트리 루트)
  └─ i440FX-pcihost (노스브리지 = PCI 호스트 브리지)  ← CPU와 연결
       └─ pci.0 (PCI 루트 버스)                      ← 여기에 PCI 장치들이 매달림
            ├─ PIIX4_PM      (전원 관리)
            ├─ piix3-ide     (IDE 컨트롤러)
            │    └─ IDE 버스
            │         └─ 하드디스크 등 IDE 장치
            └─ ... (그 외 많은 PCI 디바이스)

 

bus: pci.0
  type PCI
  dev: PIIX4_PM, id ""
    gpio-out "smi-irq" 1
    gpio-out "" 1
    smb_io_base = 1792 (0x700)
    disable_s3 = 0 (0x0)
    disable_s4 = 0 (0x0)
    s4_val = 2 (0x2)
    acpi-pci-hotplug-with-bridge-support = true
    acpi-root-pci-hotplug = true
    memory-hotplug-support = true
    smm-compat = false
    smm-enabled = true
    x-not-migrate-acpi-index = false
    addr = 01.3
    romfile = ""
    romsize = 4294967295 (0xffffffff)
    rombar = 1 (0x1)
    multifunction = false
    x-pcie-lnksta-dllla = true
    x-pcie-extcap-init = true
    failover_pair_id = ""
    acpi-index = 0 (0x0)
    class Bridge, addr 00:01.3, pci id 8086:7113 (sub 1af4:1100)
    bus: i2c
      ……
  dev: piix3-ide, id ""
    addr = 01.1
    romfile = ""
    romsize = 4294967295 (0xffffffff)
    rombar = 1 (0x1)
    multifunction = false
    x-pcie-lnksta-dllla = true
    x-pcie-extcap-init = true
    failover_pair_id = ""
    acpi-index = 0 (0x0)
    class IDE controller, addr 00:01.1, pci id 8086:7010 (sub 1af4:1100)
    bar 4: i/o at 0xc040 [0xc04f]
    bus: ide.1

앞에서 PCI 버스에 IDE 컨트롤러(piix3-ide) 같은 컨트롤러가 붙고, 그 아래 다시 하위 버스와 장치가 달린다고 했는데

컨트롤러를 거치지 않고 PCI 버스에 곧장 붙는 장치도 있다.

  • e1000 — 인텔의 기가비트 네트워크 카드(NIC) 에뮬레이션. QEMU의 대표적인 가상 랜카드입니다.
  • VGA — 그래픽 카드(디스플레이) 디바이스.

즉 디바이스 트리에서 두 가지 패턴이 있다

pci.0 (PCI 루트 버스)
  ├─ piix3-ide (컨트롤러)      → 아래에 IDE 버스 → 하드디스크   (간접: 버스→컨트롤러→버스→장치)
  ├─ e1000 (네트워크 카드)                                      (직접: 버스에 바로)
  └─ VGA (그래픽 카드)                                          (직접: 버스에 바로)
dev: piix3-ide, id ""
  addr = 01.1
  romfile = ""
  romsize = 4294967295 (0xffffffff)
  rombar = 1 (0x1)
  multifunction = false
  x-pcie-lnksta-dllla = true
  x-pcie-extcap-init = true
  failover_pair_id = ""
  acpi-index = 0 (0x0)
  class IDE controller, addr 00:01.1, pci id 8086:7010 (sub 1af4:1100)
  bar 4: i/o at 0xc040 [0xc04f]
  bus: ide.1
    type IDE
    dev: ide-cd, id ""
      drive = "ide1-cd0"
      backend_defaults = "auto"
      logical_block_size = 512 (512 B)
      physical_block_size = 512 (512 B)
      min_io_size = 0 (0 B)
      opt_io_size = 0 (0 B)
      discard_granularity = 512 (512 B)
      write-cache = "auto"
      share-rw = false
      rerror = "auto"
      werror = "auto"
      ver = "2.5+"
      wwn = 0 (0x0)
      serial = "QM00003"
      model = ""
      unit = 0 (0x0)
  bus: ide.0
    type IDE
dev: e1000, id ""
  mac = "52:54:00:12:34:56"
  netdev = "hub0port0"
  autonegotiation = true
  mitigation = true
  extra_mac_registers = true
  migrate_tso_props = true
  init-vet = true
  addr = 03.0
  romfile = "efi-e1000.rom"
  romsize = 262144 (0x40000)
  rombar = 1 (0x1)
  multifunction = false
  x-pcie-lnksta-dllla = true
  x-pcie-extcap-init = true
  failover_pair_id = ""
  acpi-index = 0 (0x0)
  class Ethernet controller, addr 00:03.0, pci id 8086:100e (sub 1af4:1100)
  bar 0: mem at 0xfebc0000 [0xfebdffff]
  bar 1: i/o at 0xc000 [0xc03f]
  bar 6: mem at 0xffffffffffffffff [0x3fffe]
dev: VGA, id ""
  vgamem_mb = 16 (0x10)
  mmio = true
  qemu-extended-regs = true
  edid = true
  xres = 1280 (0x500)
  yres = 800 (0x320)
  xmax = 0 (0x0)
  ymax = 0 (0x0)
  refresh_rate = 0 (0x0)
  global-vmstate = false
  addr = 02.0
  romfile = "vgabios-stdvga.bin"
  romsize = 65536 (0x10000)
  rombar = 1 (0x1)
  multifunction = false
  x-pcie-lnksta-dllla = true
  x-pcie-extcap-init = true
  failover_pair_id = ""
  acpi-index = 0 (0x0)
  class VGA controller, addr 00:02.0, pci id 1234:1111 (sub 1af4:1100)
  bar 0: mem at 0xfd000000 [0xfdffffff]
  bar 2: mem at 0xfebf0000 [0xfebf0fff]
  bar 6: mem at 0xffffffffffffffff [0xfffe]

piix3-ide가 IDE 컨트롤러였다면, PIIX3 본체(PCI-ISA 브리지) 는 PCI 버스와 ISA 버스를 잇는 다리 역할을 한다.

pci.0 (PCI 루트 버스)
  └─ PIIX3 (PCI-ISA 브리지)
       └─ ISA 버스 (isa.0)
            ├─ ISA 디바이스 1
            ├─ ISA 디바이스 2
            └─ ... (키보드 컨트롤러, 시리얼 포트, RTC, PIT 타이머 등 전통적 장치들)

즉 "PCI 버스 → PIIX3 브리지 → ISA 버스 → ISA 장치들" 로, 앞서 IDE에서 봤던 "버스→컨트롤러/브리지→하위 버스→장치"의 중첩 패턴이 ISA 쪽에서도 똑같이 반복된다.

ISA 버스에는 보통 옛날 방식의 저속 장치들(키보드/마우스 컨트롤러, 시리얼·병렬 포트, RTC 시계, PIT 타이머, PIC 인터럽트 컨트롤러 등)이 붙는다.

이게 바로 i440fx 하드웨어 구조도에서 봤던 "사우스브리지(PIIX3) → ISA Bus → ISA Device/Slots" 부분이 qtree 출력으로 실제 찍혀 나오는 부분이다.

dev: PIIX3, id ""
  addr = 01.0
  romfile = ""
  romsize = 4294967295 (0xffffffff)
  rombar = 1 (0x1)
  multifunction = true
  x-pcie-lnksta-dllla = true
  x-pcie-extcap-init = true
  failover_pair_id = ""
  acpi-index = 0 (0x0)
  class ISA bridge, addr 00:01.0, pci id 8086:7000 (sub 1af4:1100)
  bus: isa.0
    type ISA
    dev: port92, id ""
      gpio-out "a20" 1
    dev: vmmouse, id ""
    dev: vmport, id ""
      x-read-set-eax = true
      x-signal-unsupported-cmd = true
      x-report-vmx-type = true
      x-cmds-v2 = true
      vmware-vmx-version = 6 (0x6)
      vmware-vmx-type = 2 (0x2)
    dev: i8042, id ""
      gpio-in "ps2-mouse-input-irq" 1
      gpio-in "ps2-kbd-input-irq" 1
      gpio-out "" 2
      gpio-out "a20" 1
      extended-state = true
      kbd-throttle = false
      kbd-irq = 1 (0x1)
      mouse-irq = 12 (0xc)
    dev: isa-fdc, id ""
      iobase = 1008 (0x3f0)
      ……

 

PCI 버스에는 양쪽 끝이 있다.

한쪽은 CPU/메모리(호스트) 쪽이고, 다른 쪽은 실제 PCI 장치들(e1000, VGA 등) 쪽이다.

노스브리지(i440FX)는 이 중 호스트 쪽 끝, 즉 "CPU에서 PCI 버스로 들어가는 입구" 역할을 하는 부분을 코드로 추상화(모델링)한 것이다.

 i440FX라는 실제 칩의 그 기능을 QEMU가 하나의 객체로 만들어 표현했다. 구체적으로는 QEMU에 이런 식으로 두 개의 객체로 나뉜다.

  • i440FX-pcihost — PCI 호스트 브리지. CPU/시스템 버스에서 PCI 버스로 넘어가는 호스트 쪽 끝.
  • PCIBus (pci.0) — 그 아래 실제 PCI 장치들이 매달리는 버스.

그래서 앞 문단들에서 본 "노스브리지(i440FX-pcihost)가 main-system-bus로 CPU에 연결되고, 아래로 pci.0를 거느린다"가 바로 이 "PCI 버스의 호스트 쪽 추상화"라는 말의 구체적 모습이다.

dev: i440FX, id ""
  addr = 00.0
  romfile = ""
  romsize = 4294967295 (0xffffffff)
  rombar = 1 (0x1)
  multifunction = false
  x-pcie-lnksta-dllla = true
  x-pcie-extcap-init = true
  failover_pair_id = ""
  acpi-index = 0 (0x0)
  class Host bridge, addr 00:00.0, pci id 8086:1237 (sub 1af4:1100)

 

qtree 구조 정리 다이어그램

 

2) 가상머신 초기화 (DEFINE_I440FX_MACHINE)

QEMU 메인보드 모의에 대응하는 타입은 MachineClass이고, 대응하는 파일은 주로 hw/core/machine.c, hw/i386/pc_piix.c다. Machine은 여기서 메인보드가 가상머신 모의 안에서의 위치를 표명한다. 여기서는 i440FX+piix3 메인보드를 예로 들어 분석을 진행한다.

머신 타입의 정의는 DEFINE_I440FX_MACHINE 매크로를 통해 완성되며, 이 매크로는 hw/i386/pc_piix.c 안에 정의되었다. 코드는 다음과 같다.

#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) \
    static void pc_init_##suffix(MachineState *machine) \
    { \
        void (*compat)(MachineState *m) = (compatfn); \
        if (compat) { \
            compat(machine); \
        } \
        pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
                 TYPE_I440FX_PCI_DEVICE); \
    } \
    DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn)

DEFINE_I440FX_MACHINE의 정의는 비교적 간단해서, 한 개의 함수와 또 한 개의 매크로 DEFINE_PC_MACHINE을 포함한다. DEFINE_PC_MACHINE은 include/hw/i386/pc.h 안에 정의되었으며, 코드는 다음과 같다.

#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \
    static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) \
    { \
        MachineClass *mc = MACHINE_CLASS(oc); \
        optsfn(mc); \
        mc->init = initfn; \
    } \
    static const TypeInfo pc_machine_type_##suffix = { \
        .name       = namestr TYPE_MACHINE_SUFFIX, \
        .parent     = TYPE_PC_MACHINE, \
        .class_init = pc_machine_##suffix##_class_init, \
    }; \
    static void pc_machine_init_##suffix(void) \
    { \
        type_register(&pc_machine_type_##suffix); \
    } \
    type_init(pc_machine_init_##suffix)

DEFINE_PC_MACHINE 매크로 안에서는, pc_machine_##suffix##_class_init라는 한 개의 함수를 정의했다.

함수의 이름으로부터 추측이 가능한데, 이것은 machine_class가 종이 위로부터의 class 초기화를 Class 대상으로 만드는 방법이다.

이 함수 안에서는, 한 개의 MachineClass 대상을 생성했는데, 이것이 바로 Class 대상이다. MachineClass 대상의 init 함수는 위에 정의된 pc_init##suffix로 설정되며, 이 함수는 machine이라는 이 종류의 모형 초기화의 함수이고, 뒷쪽에서 호출되어 사용된다고 설명한다.

이어서 DEFINE_PC_MACHINE은 pc_machine_type_##suffix라는 한 개의 TypeInfo를 정의했는데, 이것은 종이 위의 class의 원자재를 생성하는 데 사용된다. 정말 뒷쪽에 가서 type_init 함수에 의해 호출된다.

각각의 새 버전마다 모두 한 종류의 새 머신 타입을 정의한다. 여기서는 그 중의 몇몇 예를 들고, 모두 hw/i386/pc_piix.c 안에 있다.

DEFINE_I440FX_MACHINE(v7_1, "pc-i440fx-7.1", NULL,
                      pc_i440fx_7_1_machine_options);
DEFINE_I440FX_MACHINE(v7_0, "pc-i440fx-7.0", NULL,
                      pc_i440fx_7_0_machine_options);
DEFINE_I440FX_MACHINE(v6_2, "pc-i440fx-6.2", NULL,
                      pc_i440fx_6_2_machine_options);
DEFINE_I440FX_MACHINE(v1_5, "pc-i440fx-1.5", pc_compat_1_5_fn,
                      pc_i440fx_1_5_machine_options);
DEFINE_I440FX_MACHINE(v1_4, "pc-i440fx-1.4", pc_compat_1_4_fn,
                      pc_i440fx_1_4_machine_options);

가장 최신의 7.1 버전을 예로 들어 설명을 진행한다.

DEFINE_I440FX_MACHINE(v7_1, "pc-i440fx-7.1", NULL,
                      pc_i440fx_7_1_machine_options);
static void pc_init_v7_1(MachineState *machine)
{
    void (*compat)(MachineState *m) = (NULL);
    if (compat) {
        compat(machine);
    }
    pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
             TYPE_I440FX_PCI_DEVICE);
}
DEFINE_PC_MACHINE(v7_1, "pc-i440fx-7.1", pc_init_v7_1, pc_i440fx_7_1_machine_options)

 

static void pc_init_v7_1(MachineState *machine)
{
    void (*compat)(MachineState *m) = (NULL);
    if (compat) {
        compat(machine);
    }
    pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
             TYPE_I440FX_PCI_DEVICE);
}

static void pc_machine_v7_1_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);
    pc_i440fx_7_1_machine_options(mc);
    mc->init = pc_init_v7_1;
}
static const TypeInfo pc_machine_type_v7_1 = {
    .name       = "pc-i440fx-7.1" TYPE_MACHINE_SUFFIX,
    .parent     = TYPE_PC_MACHINE,
    .class_init = pc_machine_v7_1_class_init,
};
static void pc_machine_init_v7_1(void)
{
    type_register(&pc_machine_type_v7_1);
}
type_init(pc_machine_init_v7_1)

볼 수 있듯이, DEFINE_I440FX_MACHINE 매크로는 한 종류의 새 타입을 정의하는 모든 작업을 직접 완성했다.

7.1 버전에서, 그것은 "pc-i440fx-7.1-machine"이라는 이 새로운 TypeInfo를 정의했다.

 

DEFINE_I440FX_MACHINE 매크로 전개 흐름

TypeInfo 대상명은 분명히 pc_machine_type_v7_1인데  "pc-i440fx-7.1-machine"은 어디서 온 것일까

TYPE_MACHINE_SUFFIX라는 이 매크로를 대입하면 명백해진다. TYPE_MACHINE_SUFFIX의 정의는 include/hw/boards.h 안에 있다. 

#define TYPE_MACHINE_SUFFIX "-machine"

DEFINE_I440FX_MACHINE(v7_1, "pc-i440fx-7.1", NULL, pc_i440fx_7_1_machine_options)이 "pc-i440fx-7.1-machine"이라는 이 TypeInfo를 정의했다고 말할 수 있다.

 

v7_1 위쪽에 나열된 모든 DEFINE_I440FX_MACHINE(v7_1, "pc-i440fx-7.1", NULL, pc_i440fx_7_1_machine_options);와 유사한 모든 머신 타입들은 모두 QOM 타입 체인 안에 추가될 것이다.

 

3) Machine 메커니즘 큰 그림과 type_register

전체 구조도

 

DEFINE_I440FX_MACHINE 매크로 분석

hw/i386/pc_piix.c 안에 핵심 매크로 DEFINE_I440FX_MACHINE이 있다.

QEMU 버전마다 새로운 머신 타입을 하나씩 정의하게 되는데, 이전 v7.1 그리고 현재 사용 중인 v8.1을 예로 들면, 코드는 각각 다음과 같다.

DEFINE_I440FX_MACHINE(v7_1, "pc-i440fx-7.1", NULL,
                      pc_i440fx_7_1_machine_options);
DEFINE_I440FX_MACHINE(v8_1, "pc-i440fx-8.1", NULL,
                      pc_i440fx_8_1_machine_options);

 

static void pc_init_v8_1(MachineState *machine)
{
    void (*compat)(MachineState *m) = (NULL);
    if (compat) {
        compat(machine);
    }
    pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
             TYPE_I440FX_PCI_DEVICE);
}
static void pc_machine_v8_1_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);
    pc_i440fx_8_1_machine_options(mc);
    mc->init = pc_init_v8_1;
}
static const TypeInfo pc_machine_type_v8_1 = {
    .name         = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
    .parent       = TYPE_PC_MACHINE,
    .class_init   = pc_machine_v8_1_class_init,
};
static void pc_machine_init_v8_1(void)
{
    type_register(&pc_machine_type_v8_1);
}
type_init(pc_machine_init_v8_1)

 

v8.1 코드의 4단계 프로세스

① pc_machine_type_v8_1

static const TypeInfo pc_machine_type_v8_1 = {
    .name         = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
    .parent       = TYPE_PC_MACHINE,
    .class_init   = pc_machine_v8_1_class_init,
};

 

② pc_machine_v8_1_class_init

static void pc_machine_v8_1_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);
    pc_i440fx_8_1_machine_options(mc);
    mc->init = pc_init_v8_1;
}

 

③ pc_init_v8_1

static void pc_init_v8_1(MachineState *machine)
{
    void (*compat)(MachineState *m) = (NULL);
    if (compat) {
        compat(machine);
    }
    pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
             TYPE_I440FX_PCI_DEVICE);
}

 

④ pc_machine_init_v8_1

static void pc_machine_init_v8_1(void)
{
    type_register(&pc_machine_type_v8_1);
}
type_init(pc_machine_init_v8_1)

 

 

type_init 매크로 깊이 파보기

하나의 QEMU 모듈을 정의할 때 type_init을 호출한다. type_init은 매크로이고, 그 정의는 include/qemu/module.h 안에 있다.

#define type_init(function) module_init(function, MODULE_INIT_QOM)

따라서 여기 있는

type_init(pc_machine_init_v8_1)

는 실제로는

module_init(pc_machine_init_v8_1, MODULE_INIT_QOM)

이다.

코드의 정의로부터 알 수 있듯이, type_init 옆의 인자는 함수(function)다. type_init을 호출하는 것은 module_init을 호출하는 것과 같고, 여기서 함수는 pc_machine_init_v8_1이고, 타입은 MODULE_INIT_QOM이다.

module_init도 매크로다. 그 정의도 include/qemu/module.h 안에 있고, 다음과 같다.

/* This should not be used directly.  Use block_init etc. instead.  */
#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                           \
    register_module_init(function, type);                                   \
}

여기에 실제 값을 대입하면, 다음과 같이 된다.

static void __attribute__((constructor)) do_qemu_init_pc_machine_init_v8_1(void)
{
    register_module_init(pc_machine_init_v8_1, MODULE_INIT_QOM);
}

 

register_module_init 함수

위 코드로부터 알 수 있듯이, module_init은 최종적으로 register_module_init 함수를 호출한다. register_module_init 함수는 util/module.c 안에 있고, 코드는 다음과 같다.

static void init_lists(void)
{
    static int inited;
    int i;

    if (inited) {
        return;
    }

    for (i = 0; i < MODULE_INIT_MAX; i++) {
        QTAILQ_INIT(&init_type_list[i]);
    }

    QTAILQ_INIT(&dso_init_list);

    inited = 1;
}

static ModuleTypeList *find_type(module_init_type type)
{
    init_lists();

    return &init_type_list[type];
}

void register_module_init(void (*fn)(void), module_init_type type)
{
    ModuleEntry *e;
    ModuleTypeList *l;

    e = g_malloc0(sizeof(*e));
    e->init = fn;
    e->type = type;

    l = find_type(type);

    QTAILQ_INSERT_TAIL(l, e, node);
}

 

module_init_type의 정의는 include/qemu/module.h 안에 있고, 다음과 같다

typedef enum {
    MODULE_INIT_MIGRATION,
    MODULE_INIT_BLOCK,
    MODULE_INIT_OPTS,
    MODULE_INIT_QOM,
    MODULE_INIT_TRACE,
    MODULE_INIT_XEN_BACKEND,
    MODULE_INIT_LIBQOS,
    MODULE_INIT_FUZZ_TARGET,
    MODULE_INIT_MAX,
} module_init_type;

MODULE_INIT_QOM 이 타입에 속하는 것은, Module 리스트인 ModuleTypeList(바로 위 코드의 init_type_list이고, 타입은 ModuleTypeList)가 하나 있고, 리스트 안에는 한 항목 한 항목의 ModuleEntry가 있다.

register_module_init 함수 안에서는 매 항목의 init 함수를 함수 인자 중의 fn(첫 번째 인자)으로 설정(초기화)한다. 여기서의 모듈의 init 함수가 바로 pc_machine_init_v8_1이다.

void register_module_init(void (*fn)(void), module_init_type type)
{
    ModuleEntry *e;
    ModuleTypeList *l;

    e = g_malloc0(sizeof(*e));
    e->init = fn;
    e->type = type;

    l = find_type(type);

    QTAILQ_INSERT_TAIL(l, e, node);
}

 

ModuleTypeList 자료구조 다이어그램

 

당연히, MODULE_INIT_QOM 이런 타입은 아주아주 많은 module이 있을 거고, type_init을 호출하는 모든 곳에서 MODULE_INIT_QOM 타입의 Module을 하나씩 등록한다.

 

module_call_init 함수 - 진짜 호출되는 곳

type_init도 그렇고 register_module_init 함수도 결국 등록만 완료한 거라고 할 수 있고, 그 module의 init 함수 포인터가 가리키는 콜백 함수를 설정한 것일 뿐이다. 진짜로 이 콜백 함수를 호출하는 곳은 module_call_init 함수 안에 있고, 이 함수도 util/module.c 안에 있다. 코드는 다음과 같다.

void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    if (modules_init_done[type]) {
        return;
    }

    l = find_type(type);

    QTAILQ_FOREACH(e, l, node) {
        e->init();
    }

    modules_init_done[type] = true;
}

 

module_call_init 함수 안에서, MODULE_INIT_QOM 이런 타입에 대응되는 ModuleTypeList를 찾아낸다.

static ModuleTypeList *find_type(module_init_type type)
{
    init_lists();

    return &init_type_list[type];
}

그 다음에는 (해당) 리스트의 모든 ModuleEntry를 찾아내서, 각 ModuleEntry의 init 함수를 호출한다. 대응되는 것이 module_call_init 함수 안의 이 한 줄이다.

QTAILQ_FOREACH(e, l, node) {
    e->init();
}

여기서 주의할 점은, module_call_init 함수가 호출되는 이 단계에서, 모든 Module의 init 함수가 이미 호출됐다는 거다. 즉, 뒷글에서 많은 Module을 보게 될 텐데, 그것들을 볼 때 인식해야 할 점은, 그 init 함수는 여기서 이미 호출된 것이다.

 

정리: type_init부터 pc_machine_init_v8_1까지 흐름

type_init(pc_machine_init_v8_1)

여기 type_init은 module_call_init 함수 안의

e->init();

이고 실제로 호출되는 것은 register_module_init 함수 안에서 설정된

e->init = fn;

다. 즉 pc_machine_init_v8_1 함수다.

static void pc_machine_init_v8_1(void)
{
    type_register(&pc_machine_type_v8_1);
}
type_init(pc_machine_init_v8_1)

 

전체 호출 흐름 다이어그램

 

핵심 개념 요약

 

type_register 함수 분석

TypeImpl *type_register(const TypeInfo *info)
{
    assert(info->parent);
    return type_register_internal(info);
}
static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

type_new 함수도 qom/object.c 안에 있다.

static TypeImpl *type_new(const TypeInfo *info)
{
    TypeImpl *ti = g_malloc0(sizeof(*ti));
    int i;

    g_assert(info->name != NULL);

    if (type_table_lookup(info->name) != NULL) {
        fprintf(stderr, "Registering `%s' which already exists\n", info->name);
        abort();
    }

    ti->name = g_strdup(info->name);
    ti->parent = g_strdup(info->parent);

    ti->class_size = info->class_size;
    ti->instance_size = info->instance_size;
    ti->instance_align = info->instance_align;

    ti->class_init = info->class_init;
    ti->class_base_init = info->class_base_init;
    ti->class_data = info->class_data;

    ti->instance_init = info->instance_init;
    ti->instance_post_init = info->instance_post_init;
    ti->instance_finalize = info->instance_finalize;

    ti->abstract = info->abstract;

    for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
        ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
    }
    ti->num_interfaces = i;

    return ti;
}

 

type_table_add 함수도 qom/object.c 안에 있고, 코드는 다음과 같다.

static void type_table_add(TypeImpl *ti)
{
    assert(!enumerating_types);
    g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}

 

type_register 호출 체인 다이어그램

 

클래스와 객체의 비유

매 Module마다 어떤 디바이스를 시뮬레이션해야 하니까, 그 디바이스를 표현할 TypeImpl을 하나 정의해야 한다. 이건 사실 객체 지향 프로그래밍의 사고방식인데, 다만 여기서는 순수 C 언어로 구현했기 때문에, 변형된 방식으로 클래스와 객체를 구현해야 한다.

static void pc_machine_init_v8_1(void)
{
    type_register(&pc_machine_type_v8_1);
}
type_init(pc_machine_init_v8_1)

pc_machine_init_v8_1 함수는 pc_machine_type_v8_1을 등록하는데, 이렇게 해서 하나의 클래스를 동적으로 정의했다고 볼 수 있다.

static const TypeInfo pc_machine_type_v8_1 = {
    .name         = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
    .parent       = TYPE_PC_MACHINE,
    .class_init   = pc_machine_v8_1_class_init,
};

이 클래스의 이름은 "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX, 즉 pc-i440fx-8.1-machine이다.

이 클래스는 부모 클래스 TYPE_PC_MACHINE (즉 "generic-pc-machine") 을 가진다.

이 클래스의 초기화는 pc_machine_v8_1_class_init 함수를 호출해야 한다.

 

여기의 호출 체인은

pc_machine_init_v8_1
    type_register
        type_register_internal
static void pc_machine_init_v8_1(void)
{
    type_register(&pc_machine_type_v8_1);
}
static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

type_register_internal 함수에서는, pc_machine_type_v8_1 이 TypeInfo에 근거해서, 이 새로 등록된 클래스를 표현하는 TypeImpl을 하나 생성한다.

즉, TypeImpl이 진짜로 선언하려고 하는 그 class라는 거다. TypeInfo 구조와 TypeImpl 구조의 정의를 각각 살펴보자.

 

struct TypeInfo
{
    const char *name;
    const char *parent;

    size_t instance_size;
    size_t instance_align;
    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;
    size_t class_size;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);
    void *class_data;

    InterfaceInfo *interfaces;
};

 

struct TypeImpl
{
    const char *name;

    size_t class_size;

    size_t instance_size;
    size_t instance_align;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);

    void *class_data;

    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;

    const char *parent;
    TypeImpl *parent_type;

    ObjectClass *class;

    int num_interfaces;
    InterfaceImpl interfaces[MAX_INTERFACES];
};

정의로 봐서, TypeInfo 구조와 TypeImpl 구조는 꽤 비슷하다.

 

TypeInfo vs TypeImpl 비교 다이어그램

 

QEMU 안에는 모든 정의된 클래스를 저장하는 전역 해시 테이블 type_table이 하나 있다.

type_new 함수 안에서는, 먼저 type_table에서 이름으로 어떤 클래스(여기서는 pc_machine_type_v8_1)를 찾아본다.

만약 찾으면, 이 클래스가 이미 등록되었음을 의미하고, 에러를 내고 현재 프로세스를 종료한다. 만약 못 찾으면, 이건 새로운 클래스라는 의미고, TypeInfo 안의 정보를 TypeImpl 안에 채워 넣는다.

 

그 후에, type_register_internal 함수는 type_table_add 함수를 호출해서, 이 클래스를 전역 테이블 안에 등록한다.

static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

 

Java 리플렉션 메커니즘과의 비교

여기는 Java의 리플렉션(Reflection) 메커니즘과 좀 비슷하다.

Java에서는, 어떤 클래스에 대해, 우선 코드를 작성할 때 class xxx의 정의를 하나 작성해야 하고, 컴파일하고 나면 .class 파일 안에 놓인다. 아직 정의로만 존재하는 상태에 있는 거다.

그 다음에, Java 안에는 Class 객체가 있어서, 정의 위의 class xxx를 읽고 표현하는 데 사용되고, 이렇게 해서 진짜 객체를 생성한다.

 

QEMU 안에도 비슷한 과정이 있다. class_init이 XXXClass를 생성하는데, 이건 Java의 Class xxx에 해당한다.

TypeImpl 안에는 또 instance_init 함수가 하나 있어서, 생성자 함수에 해당한다.

XXXClass에 근거해서 Object를 생성하는 데 쓰이는데, 이건 Java 리플렉션가 최종적으로 객체를 생성하는 것에 해당한다. 그리고 생성자 함수와 대응되는 게 또 instance_finalize인데, 소멸자 함수에 해당한다.

 

Java 리플렉션 vs QEMU QOM 메커니즘 비교

 

QEMU 메인 흐름

QEMU의 옛날 버전에서는, 메인 함수 main에서 select_machine 함수를 직접 호출했고, 새 버전에서는 다음과 같은 호출 흐름이다.

main()
    qemu_main()
        qemu_init()
            qemu_create_machine
                select_machine()
static void qemu_create_machine(QDict *qdict)
{
    MachineClass *machine_class = select_machine(qdict, &error_fatal);
    object_set_machine_compat_props(machine_class->compat_props);

    current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));
    object_property_add_child(object_get_root(), "machine",
                              OBJECT(current_machine));
    object_property_add_child(container_get(OBJECT(current_machine),
                                            "/unattached"),
                              "sysbus", OBJECT(sysbus_get_default()));

    if (machine_class->minimum_page_bits) {
        if (!set_preferred_target_page_bits(machine_class->minimum_page_bits)) {
            /* This would be a board error: specifying a minimum smaller than
             * a target's compile-time fixed setting.
             */
            g_assert_not_reached();
        }
    }

    cpu_exec_init_all();
    page_size_init();

    if (machine_class->hw_version) {
        qemu_set_hw_version(machine_class->hw_version);
    }

    /*
     * Get the default machine options from the machine if it is not already
     * specified either by the configuration file or by the command line.
     */
    if (machine_class->default_machine_opts) {
        QDict *default_opts =
            keyval_parse(machine_class->default_machine_opts, NULL, NULL,
                         &error_abort);
        qemu_apply_legacy_machine_options(default_opts);
        object_set_properties_from_keyval(OBJECT(current_machine), default_opts,
                                          false, &error_abort);
        qobject_unref(default_opts);
    }
}

 

MachineClass *machine_class = select_machine(qdict, &error_fatal);

이름이 의미하는 대로, select_machine 함수의 역할은 MachineClass를 하나 선택하는 거다.

이건 사용자가 지정할 수도 있고, 사용자가 지정하지 않으면 시스템 기본값을 채택한다.

후자라면, QEMU 최신 버전 번호에 대응되는 머신 타입을 기본 설정으로 한다. 여기서 분석할 버전은 qemu-8.1.4이기 때문에, 따라서 기본 머신 타입은 pc-i440fx-8.1-machine이다. 그리고 이건 바로 다음에 대응된다

static const TypeInfo pc_machine_type_v8_1 = {
    .name         = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
    .parent       = TYPE_PC_MACHINE,
    .class_init   = pc_machine_v8_1_class_init,
};
static void pc_machine_init_v8_1(void)
{
    type_register(&pc_machine_type_v8_1);
}

 

static MachineClass *select_machine(QDict *qdict, Error **errp)
{
    const char *optarg = qdict_get_try_str(qdict, "type");
    GSList *machines = object_class_get_list(TYPE_MACHINE, false);
    MachineClass *machine_class;
    Error *local_err = NULL;

    if (optarg) {
        machine_class = find_machine(optarg, machines);
        qdict_del(qdict, "type");
        if (!machine_class) {
            error_setg(&local_err, "unsupported machine type");
        }
    } else {
        machine_class = find_default_machine(machines);
        if (!machine_class) {
            error_setg(&local_err, "No machine specified, and there is no default");
        }
    }

    g_slist_free(machines);
    if (local_err) {
        error_append_hint(&local_err,
                          "Use -machine help to list supported machines\n");
        error_propagate(errp, local_err);
    }
    return machine_class;
}

위에서 말한 것처럼, select_machine 함수 안에서는, MachineClass를 생성하는 두 가지 방법이 있다.

하나는 find_machine 함수를 호출하는 방식인데, QEMU 명령줄 파라미터를 해석해서 MachineClass를 생성하는 거고, 즉 사용자가 지정하는 방식이고

또 다른 하나는 find_default_machine 함수를 통해 기본 MachineClass를 하나 찾는 거고, 즉 시스템 기본 방식이다.

 

select_machine 분기 다이어그램

사용자 지정이든 시스템 기본 방식이든, 모두 먼저 object_class_get_list 함수를 호출해서 MachineClass 리스트를 하나 얻고, 그 다음에 그 안에서 찾아야 한다.

GSList *machines = object_class_get_list(TYPE_MACHINE, false);
GSList *object_class_get_list(const char *implements_type,
                              bool include_abstract)
{
    GSList *list = NULL;

    object_class_foreach(object_class_get_list_tramp,
                         implements_type, include_abstract, &list);
    return list;
}
void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque),
                          const char *implements_type, bool include_abstract,
                          void *opaque)
{
    OCFData data = { fn, implements_type, include_abstract, opaque };

    enumerating_types = true;
    g_hash_table_foreach(type_table_get(), object_class_foreach_tramp, &data);
    enumerating_types = false;
}

전역 테이블 type_table_get() 안에서, 각 항목의 TypeImpl에 대해, 모두 object_class_foreach_tramp를 실행한다.

static void object_class_foreach_tramp(gpointer key, gpointer value,
                                       gpointer opaque)
{
    OCFData *data = opaque;
    TypeImpl *type = value;
    ObjectClass *k;

    type_initialize(type);
    k = type->class;

    if (!data->include_abstract && type->abstract) {
        return;
    }

    if (data->implements_type &&
        !object_class_dynamic_cast(k, data->implements_type)) {
        return;
    }

    data->fn(k, data->opaque);
}

 

object_class_foreach_tramp 함수 안에서, type_initialize 함수를 호출한다.

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    if (ti->class) {
        return;
    }

    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /* Any type with zero instance_size is implicitly abstract.
     * This means interface types are all abstract.
     */
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
        assert(ti->instance_size == 0);
        assert(ti->abstract);
        assert(!ti->instance_init);
        assert(!ti->instance_post_init);
        assert(!ti->instance_finalize);
        assert(!ti->num_interfaces);
    }
    ti->class = g_malloc0(ti->class_size);

    parent = type_get_parent(ti);
    if (parent) {
        type_initialize(parent);
        GSList *e;
        int i;

        g_assert(parent->class_size <= ti->class_size);
        g_assert(parent->instance_size <= ti->instance_size);
        memcpy(ti->class, parent->class, parent->class_size);
        ti->class->interfaces = NULL;

        for (e = parent->class; e = e->next) {
            InterfaceClass *iface = e->data;
            ObjectClass *klass = OBJECT_CLASS(iface);

            type_initialize_interface(ti, iface->interface_type, klass->type);
        }

        for (i = 0; i < ti->num_interfaces; i++) {
            TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
            if (!t) {
                error_report("missing interface '%s' for object '%s'",
                             ti->interfaces[i].typename, parent->name);
                abort();
            }
            for (e = ti->class->interfaces; e; e = e->next) {
                TypeImpl *target_type = OBJECT_CLASS(e->data)->type;

                if (type_is_ancestor(target_type, t)) {
                    break;
                }
            }

            if (e) {
                continue;
            }

            type_initialize_interface(ti, t, t);
        }
    }

    ti->class->properties = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
                                                  object_property_free);
    ti->class->type = ti;

    while (parent) {
        if (parent->class_base_init) {
            parent->class_base_init(ti->class, ti->class_data);
        }
        parent = type_get_parent(parent);
    }

    if (ti->class_init) {
        ti->class_init(ti->class, ti->class_data);
    }
}

type_initialize 함수 안에서 (마지막 부분), class_init을 호출한다. 정의 위의 class를 TypeImpl 안에서 ObjectClass로 만든다. ObjectClass는 모든 클래스의 조상이고, MachineClass는 그것의 자식 클래스다.

여기서 이전에 등록된 TypeImpl을 분명히 찾을 수 있고, 그 class_init 함수를 호출한다.

 

따라서, pc_machine_##suffix##_class_init (이 예에서는 pc_machine_v8_1_class_init) 이 호출된다.

static void pc_machine_v8_1_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);
    pc_i440fx_8_1_machine_options(mc);
    mc->init = pc_init_v8_1;
}

type_initialize 재귀 호출 다이어그램

 

pc_i440fx_8_1_machine_options와 pc_i440fx_machine_options

이 함수 안에서, pc_i440fx_machine_options가 진짜로 pc_i440fx_8_1machine_options에 의해 호출되고, 이로써 MachineClass를 초기화한다.

static void pc_i440fx_8_1_machine_options(MachineClass *m)
{
    pc_i440fx_machine_options(m);
    m->alias = "pc";
    m->is_default = true;
}

 

static void pc_i440fx_machine_options(MachineClass *m)
{
    PCMachineClass *pcmc = PC_MACHINE_CLASS(m);
    pcmc->pci_root_uid = 0;
    pcmc->default_cpu_version = 1;

    m->family = "pc_piix";
    m->desc = "Standard PC (i440FX + PIIX, 1996)";
    m->default_machine_opts = "firmware=bios-256k.bin";
    m->default_display = "std";
    m->default_nic = "e1000";
    m->no_parallel = !module_object_class_by_name(TYPE_ISA_PARALLEL);
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_RAMFB_DEVICE);
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_VMBUS_BRIDGE);
}

pc_machine_v8_1_class_init 함수는 그 다음에 MachineClass의 init 함수를 pc_init_##suffix로 설정한다.

mc->init = pc_init_v8_1;

따라서, select_machine 함수 실행이 끝나면, 하나의 MachineClass가 있게 된다.

 

typedef struct MachineClass MachineClass;
struct MachineClass {
    /*< private >*/
    ObjectClass parent_class;
    /*< public >*/

    const char *family; /* NULL iff @name identifies a standalone machtype */
    char *name;
    const char *alias;
    const char *desc;
    const char *deprecation_reason;

    void (*init)(MachineState *state);
    void (*reset)(MachineState *state, ShutdownCause reason);
    void (*wakeup)(MachineState *state);
    int (*kvm_type)(MachineState *machine, const char *arg);

    BlockInterfaceType block_default_type;
    int units_per_default_bus;
    int max_cpus;
    int min_cpus;
    int default_cpus;
    unsigned int no_serial:1,
        no_parallel:1,
        no_floppy:1,
        no_cdrom:1,
        no_sdcard:1,
        pci_allow_0_address:1,
        legacy_fw_cfg_order:1;
    bool is_default;
    const char *default_machine_opts;
    const char *default_boot_order;
    const char *default_display;
    const char *default_nic;
    GPtrArray *compat_props;
    const char *hw_version;
    ram_addr_t default_ram_size;
    const char *default_cpu_type;
    bool default_kernel_irqchip_split;
    bool option_rom_has_mr;
    bool rom_file_has_mr;
    int minimum_page_bits;
    bool has_hotpluggable_cpus;
    bool ignore_memory_transaction_failures;
    int numa_mem_align_shift;
    const char **valid_cpu_types;
    strList *allowed_dynamic_sysbus_devices;
    bool auto_enable_numa_with_memhp;
    bool auto_enable_numa_with_memdev;
    bool ignore_boot_device_suffixes;
    bool smbus_no_migration_support;
    bool nvdimm_supported;
    bool numa_mem_supported;
    bool auto_enable_numa;
    bool cpu_cluster_has_numa_boundary;
    SMPCompatProps smp_props;
    const char *default_ram_id;

    HotplugHandler *(*get_hotplug_handler)(MachineState *machine,
                                           DeviceState *dev);
    bool (*hotplug_allowed)(MachineState *state, DeviceState *dev,
                            Error **errp);
    CpuInstanceProperties (*cpu_index_to_instance_props)(MachineState *machine,
                                                          unsigned cpu_index);
    const CPUArchIdList *(*possible_cpu_arch_ids)(MachineState *ms);
    int64_t (*get_default_cpu_node_id)(const MachineState *ms, int idx);
    ram_addr_t (*fixup_ram_size)(ram_addr_t size);
};

정리하자면 QEMU를 실행할 때 -machine pc 또는 -machine q35 같은 옵션으로 어떤 종류의 가상 메인보드(머신 타입) 를 쓸지 지정한다. select_machine 함수가 하는 일이 바로 그거다.

사용자가 지정한 이름(또는 기본값)에 맞는 머신 타입을 골라내는것

여기서 두 가지 개념을 구분해야 한다 

  • MachineClass — 머신 "종류(틀)"의 정보. "i440fx 타입은 이런 속성을 갖고, 이런 초기화 함수를 쓴다"는 설계도/명세. (Java로 치면 Class 객체)
  • MachineState — 실제로 생성된 머신 "인스턴스". 그 설계도로 찍어낸 진짜 가동되는 가상머신 객체 (Java로 치면 new로 만든 인스턴스)

select_machine 실행

→ 사용자가 고른 머신 타입(예: pc-i440fx)의 MachineClass를 찾아냄 

→ 이 시점: "어떤 종류의 머신을 만들지(MachineClass)"는 정해졌지만, 아직 실제 머신 객체(MachineState)는 안 만들어진 상태

 

4) select_machine → MachineState 객체 생성

qemu_create_machine 함수로 돌아가서,

select_machine 함수 실행이 끝나고, 즉 하나의 MachineClass를 얻은 후에, 다음으로 와서 이 코드를 다시 보자.

current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));

 

MACHINE 매크로 분석

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE);
}

OBJECT_CHECK 매크로가 펼쳐진 후의 함수 코드는 다음과 같다.

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return ((MachineState*)object_dynamic_cast_assert(OBJECT(obj), ("machine"), __FILE__, __LINE__, __func__));
}

 

이제 다시 돌아가서 qemu_create_machine 함수 (softmmu/vl.c) 가 MACHINE 함수를 호출할 때, 전달한 실제 인자를 보자.

 

object_new(object_class_get_name(OBJECT_CLASS(machine_class))).
Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);

    return object_new_with_type(ti);
}
Object *object_new_with_class(ObjectClass *klass)
{
    return object_new_with_type(klass->type);
}

볼 수 있듯이, 어느 함수든, 최종적으로는 모두 object_new_with_type 함수를 호출한다.

static Object *object_new_with_type(Type type)
{
    Object *obj;
    size_t size, align;
    void (*obj_free)(void *);

    g_assert(type != NULL);
    type_initialize(type);

    size = type->instance_size;
    align = type->instance_align;

    /*
     * Do not use qemu_memalign unless required.  Depending on the
     * implementation, extra alignment implies extra overhead.
     */
    if (likely(align <= __alignof__(qemu_max_align_t))) {
        obj = g_malloc(size);
        obj_free = g_free;
    } else {
        obj = qemu_memalign(align, size);
        obj_free = qemu_vfree;
    }

    object_initialize_with_type(obj, size, type);
    obj->free = obj_free;

    return obj;
}

object_new 함수로 돌아가서, TypeImpl의 instance_init이 호출되고, 하나의 객체를 생성한다. current_machine이 바로 이 객체이고, 그 타입은 MachineState다.

Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);

    return object_new_with_type(ti);
}

 

object_new 호출 체인 다이어그램

 

객체 생성 흐름 시퀀스 다이어그램

 

핵심 4단계 요약

 

함수 호출 트리

 

종합 흐름

 

한눈에 보는 전체 데이터 플로우

 

5) select_machine~MachineState 상세 추적

앞서 qemu_create_machine 함수의 첫 번째 단계인 select_machine 함수의 진입부를 분석했고, 그 안에서 호출되는 object_class_get_list와 object_class_foreach 함수까지 다뤘다.

이번 정리에서는 그 안에서 호출되는 3개의 함수를 시작으로, MACHINE 매크로의 정체를 파헤치고, 마지막으로 MachineState 객체가 생성되어 current_machine에 할당되기까지의 전 과정을 다룬다.

 

이번에 다룰 내용을 큰 그림으로 먼저 보자.

 

 

1. 3개 함수 정리: type_table_get, object_class_foreach_tramp, object_class_get_list_tramp

이전에 3개의 함수를 명확하게 정리할 필요가 있다고 말했다.

(1) object_class_get_list_tramp;

(2) object_class_foreach_tramp;

(3) type_table_get.

 

GSList *object_class_get_list(const char *implements_type,
                              bool include_abstract)
{
    GSList *list = NULL;

    object_class_foreach(object_class_get_list_tramp,
                         implements_type, include_abstract, &list);
    return list;
}

 

먼저 첫 번째 함수 type_table_get을 보자.

static GHashTable *type_table_get(void)
{
    static GHashTable *type_table;

    if (type_table == NULL) {
        type_table = g_hash_table_new(g_str_hash, g_str_equal);
    }

    return type_table;
}

전역 type_table_get()이 반환하는 type_table 안에서, 모든 TypeImpl 항목에 대해 object_class_foreach_tramp를 실행한다.

 

다음으로 두 번째 함수 object_class_foreach_tramp를 보자.

object_class_foreach_tramp 함수에서 OCFData *data는 이 함수의 3번째 매개변수 gpointer opaque에 의해 할당되는데, 이 매개변수는 실제로 object_class_foreach 함수 내의 data를 가리킨다. 즉

OCFData data = { fn, implements_type, include_abstract, opaque }

 

상위 문맥의 실제 값을 대입하면 최종적으로 다음과 같다.

OCFData data = { fn, TYPE_MACHINE, false, &list }.

자세한 내용은 이전에 다룬 softmmu/vl.c의 select_machine 함수를 보면 된다

static MachineClass *select_machine(QDict *qdict, Error **errp)
{
    const char *optarg = qdict_get_try_str(qdict, "type");
    GSList *machines = object_class_get_list(TYPE_MACHINE, false);
    ......
    return machine_class;
}

우리는 이전 글에서 QOM에 대해 다룰 때 type_initialize 함수를 자세히 해석한 적이 있다.

그것의 역할은 클래스의 초기화이며, 여기서는 당연히 TYPE_MACHINE 즉 "machine" 클래스의 초기화다.

type_initialize 함수는 class_init 함수를 호출하여 종이 위의 class 즉 TypeImpl을 ObjectClass로 변환한다.

이전에도 언급했듯이, ObjectClass는 모든 Class 클래스의 조상이고, 여기서 MachineClass는 그것의 자식 클래스다.

 

object_class_foreach_tramp 함수의 마지막

data->fn(k, data->opaque);

 

에 실제 값을 대입하면 object_class_get_list_tramp(type->class, &list)가 된다.

 

마지막으로 세 번째 함수 object_class_get_list_tramp를 보자.

static void object_class_get_list_tramp(ObjectClass *klass, void *opaque)
{
    GSList **list = opaque;

    *list = g_slist_prepend(*list, klass);
}

명령줄에 "-machine xx-xxx-xxx"를 전달하든, 관련 값을 지정하지 않고 기본값을 사용하든, 결국에는 이전에 이미 등록된 TypeImpl을 찾을 수 있고, 그것의 class_init 함수를 호출한다.

따라서 pc_machine_##suffix##class_init 즉 .class_init = pc_machine_v7_1_class_init이 호출된다. 관련 코드를 다시 제시하면 다음과 같다.

static void pc_init_v7_1(MachineState *machine)
{
    void (*compat)(MachineState *m) = (NULL);
    if (compat) {
        compat(machine);
    }
    pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
                      TYPE_I440FX_PCI_DEVICE);
}

static void pc_machine_v7_1_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);
    pc_i440fx_7_1_machine_options(mc);
    mc->init = pc_init_v7_1;
}

static const TypeInfo pc_machine_type_v7_1 = {
    .name       = "pc-i440fx-7.1" TYPE_MACHINE_SUFFIX,
    .parent     = TYPE_PC_MACHINE,
    .class_init = pc_machine_v7_1_class_init,
};
static void pc_machine_init_v7_1(void)
{
    type_register(&pc_machine_type_v7_1);
}

pc_machine_##suffix##class_init 즉 pc_machine_v7_1_class_init 함수에서,

pc_i440fx_7_1_machine_options 함수가 비로소 진짜로 호출되어 MachineClass를 초기화하고,

MachineClass의 init 함수를 pc_init##suffix 즉 pc_init_v7_1로 설정한다.

 

즉, select_machine이 실행 완료된 후에는 하나의 MachineClass가 생기게 된다.

static void pc_i440fx_7_1_machine_options(MachineClass *m)
{
    PCMachineClass *pcmc = PC_MACHINE_CLASS(m);
    pc_i440fx_machine_options(m);
    m->alias = "pc";
    m->is_default = true;
    pcmc->default_cpu_version = 1;
    pcmc->legacy_no_rng_seed = true;
}

 

static void pc_i440fx_machine_options(MachineClass *m)
{
    PCMachineClass *pcmc = PC_MACHINE_CLASS(m);
    pcmc->default_nic_model = "e1000";
    pcmc->pci_root_uid = 0;
    m->family = "pc_piix";
    m->desc = "Standard PC (i440FX + PIIX, 1996)";
    m->default_machine_opts = "firmware=bios-256k.bin";
    m->default_display = "std";
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_RAMFB_DEVICE);
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_VMBUS_BRIDGE);
}

 

세 함수의 호출 흐름 다이어그램

 

2. select_machine 복귀: find_machine과 find_default_machine

static MachineClass *select_machine(QDict *qdict, Error **errp)
{
    const char *optarg = qdict_get_try_str(qdict, "type");
    GSList *machines = object_class_get_list(TYPE_MACHINE, false);
    MachineClass *machine_class;
    Error *local_err = NULL;

    if (optarg) {
        machine_class = find_machine(optarg, machines);
        qdict_del(qdict, "type");
        if (!machine_class) {
            error_setg(&local_err, "unsupported machine type");
        }
    } else {
        machine_class = find_default_machine(machines);
        if (!machine_class) {
            error_setg(&local_err, "No machine specified, and there is no default");
        }
    }

    g_slist_free(machines);
    if (local_err) {
        error_append_hint(&local_err, "Use -machine help to list supported machines\n");
        error_propagate(errp, local_err);
    }
    return machine_class;
}

이전 글에서 언급했듯이, select_machine 함수에서는 MachineClass를 생성하는 두 가지 방식이 있다.

하나는 find_machine 함수를 호출하는 방식으로, QEMU 명령줄 매개변수를 파싱하여 MachineClass를 생성한다. 즉 사용자 지정 방식이다.

다른 하나는 find_default_machine 함수를 통해 기본 MachineClass를 찾는 방식이다. 즉 시스템 기본 방식이다. 하나씩 살펴보자.

 

명령줄 지정 방식: find_machine

static MachineClass *find_machine(const char *name, GSList *machines)
{
    GSList *el;

    for (el = machines; el; el = el->next) {
        MachineClass *mc = el->data;

        if (!strcmp(mc->name, name) || !g_strcmp0(mc->alias, name)) {
            return mc;
        }
    }

    return NULL;
}

만약 명령줄에 지정된 이름이 이미 생성된 MachineClass의 이름이나 별칭과 같다면, 이 MachineClass 객체 mc를 반환한다. 그렇지 않으면 NULL을 반환하는데, 이는 찾지 못했다는 뜻이다.

 

시스템 기본 방식: find_default_machine

static MachineClass *find_default_machine(GSList *machines)
{
    GSList *el;
    MachineClass *default_machineclass = NULL;

    for (el = machines; el; el = el->next) {
        MachineClass *mc = el->data;

        if (mc->is_default) {
            assert(default_machineclass == NULL && "Multiple default machines");
            default_machineclass = mc;
        }
    }

    return default_machineclass;
}

보다시피, 의미는 find_machine 함수와 별 차이가 없다.

마찬가지로 machines 링크드 리스트를 순회하는데, 다만 여기서는 이름을 비교하지 않고, is_default 속성이 true인 MachineClass 객체 mc를 찾아서 검사하여 문제가 없으면 반환한다.

 

여기까지 해서 select_machine 함수는 자신의 사명을 완수했고, 최종적으로 필요한 MachineClass 객체(주소)를 반환했다.

 

select_machine의 두 가지 분기 다이어그램

 

3. MACHINE 매크로 추적

current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));

 

옛 버전 원본 소스에서 X86_MACHINE의 정의를 찾을 수 있었는데, 다음과 같다.

/* Address space used by IOAPIC device. All IOAPIC interrupts
 * will be translated to MSI messages in the address space. */
AddressSpace *ioapic_as;

} X86MachineState;

#define X86_MACHINE_MAX_RAM_BELOW_4G "max-ram-below-4g"

#define TYPE_X86_MACHINE   MACHINE_TYPE_NAME("x86")
#define X86_MACHINE(obj) \
    OBJECT_CHECK(X86MachineState, (obj), TYPE_X86_MACHINE)
#define X86_MACHINE_GET_CLASS(obj) \
    OBJECT_GET_CLASS(X86MachineClass, obj, TYPE_X86_MACHINE)
#define X86_MACHINE_CLASS(class) \
    OBJECT_CLASS_CHECK(X86MachineClass, class, TYPE_X86_MACHINE)

uint32_t x86_cpu_apic_id_from_index(PCMachineState *pcms,
                                    unsigned int cpu_index);

이로부터 X86_MACHINE의 정의가 X86MachineState 구조체와 같은 파일(include/hw/i386/x86.h)에 있으므로, MACHINE의 정의도 MachineState와 같은 파일 안에 있어야 한다는 것을 연상할 수 있었다.

 

그래서 QEMU 소스 코드 루트 디렉터리에서 "MachineState"를 검색했고, 마침내 include/hw/boards.h 파일에 위치를 확정했다.

 

또한 X86_MACHINE 매크로 정의가 위치한 include/hw/i386/x86.h 파일의 내용이 버전 업데이트로 인해 위와 달라졌으므로, 원래 X86_MACHINE 매크로 정의된 곳을 통해 새 버전에 대응하는 코드가 어떻게 변했는지 살펴보았다. 결과적으로 이 코드 조각을 찾을 수 있었다.

#define TYPE_X86_MACHINE   MACHINE_TYPE_NAME("x86")
OBJECT_DECLARE_TYPE(X86MachineState, X86MachineClass, X86_MACHINE)

이로부터 명백히 알 수 있듯이, 원래 #define X86_MACHINE(obj) OBJECT_CHECK(X86MachineState, (obj), TYPE_X86_MACHINE)이었던 코드가 새 버전에서는 OBJECT_DECLARE_TYPE(X86MachineState, X86MachineClass, X86_MACHINE)로 바뀌었다.

그러면 이와 대응하여, include/hw/boards.h 파일에서, 만약 이 OBJECT_DECLARE_TYPE을 찾을 수 있다면, 그것에 대응하는 코드가 바로 MACHINE의 정의가 있는 곳일 것이다.

이 생각대로, include/hw/boards.h에서 검색했더니, 다음 코드를 찾았다.

OBJECT_DECLARE_TYPE(MachineState, MachineClass, MACHINE)

 

4. OBJECT_DECLARE_TYPE 한 겹씩 펼치기

OBJECT_DECLARE_TYPE(MachineState, MachineClass, MACHINE)
/**
 * OBJECT_DECLARE_TYPE:
 * @InstanceType: instance struct name
 * @ClassType: class struct name
 * @MODULE_OBJ_NAME: the object name in uppercase with underscore separators
 *
 * This macro is typically used in a header file, and will:
 *   - create the typedefs for the object and class structs
 *   - register the type for use with g_autoptr
 *   - provide three standard type cast functions
 *
 * The object struct and class struct need to be declared manually.
 */
#define OBJECT_DECLARE_TYPE(InstanceType, ClassType, MODULE_OBJ_NAME) \
    typedef struct InstanceType InstanceType; \
    typedef struct ClassType ClassType; \
    \
    G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstanceType, object_unref) \
    \
    DECLARE_OBJ_CHECKERS(InstanceType, ClassType, \
                         MODULE_OBJ_NAME, TYPE_##MODULE_OBJ_NAME)

여기서는 또 두 개의 매크로가 관련되어 있다.

  • G_DEFINE_AUTOPTR_CLEANUP_FUNC
  • DECLARE_OBJ_CHECKERS

뒤의 것에 중점을 둔다.

 

/**
 * DECLARE_OBJ_CHECKERS:
 * @InstanceType: instance struct name
 * @ClassType: class struct name
 * @OBJ_NAME: the object name in uppercase with underscore separators
 * @TYPENAME: type name
 *
 * Direct usage of this macro should be avoided, and the complete
 * OBJECT_DECLARE_TYPE macro is recommended instead.
 *
 * This macro will provide the three standard type cast functions for a
 * QOM type.
 */
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
    DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
    \
    DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)

여기서는 또 DECLARE_INSTANCE_CHECKER와 DECLARE_CLASS_CHECKERS라는 두 개의 매크로 정의가 포함되어 있다.

 

/**
 * DECLARE_INSTANCE_CHECKER:
 * @InstanceType: instance struct name
 * @OBJ_NAME: the object name in uppercase with underscore separators
 * @TYPENAME: type name
 */
#define DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
    static inline G_GNUC_UNUSED InstanceType * \
    OBJ_NAME(const void *obj) \
    { return OBJECT_CHECK(InstanceType, obj, TYPENAME); }
/*
 * This macro will provide the three standard type cast functions for a
 * QOM type.
 */
#define DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME) \
    static inline G_GNUC_UNUSED ClassType * \
    OBJ_NAME##_GET_CLASS(const void *obj) \
    { return OBJECT_GET_CLASS(ClassType, obj, TYPENAME); } \
    \
    static inline G_GNUC_UNUSED ClassType * \
    OBJ_NAME##_CLASS(const void *klass) \
    { return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME); }

 

이제 각 매크로 정의는 기본적으로 다 나왔으니 (G_DEFINE_AUTOPTR_CLEANUP_FUNC는 일단 펼치지 않는다), 하나씩 펼쳐가면서, 완전히 펼쳐진 후의 내용이 무엇인지 끝까지 보자.

 

1단계

#define OBJECT_DECLARE_TYPE(InstanceType, ClassType, MODULE_OBJ_NAME) \
    typedef struct InstanceType InstanceType; \
    typedef struct ClassType ClassType; \
    \
    G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstanceType, object_unref) \
    \
    DECLARE_OBJ_CHECKERS(InstanceType, ClassType, \
                         MODULE_OBJ_NAME, TYPE_##MODULE_OBJ_NAME)

대입하면

typedef struct MachineState MachineState;
typedef struct MachineClass MachineClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(MachineState, object_unref)

DECLARE_OBJ_CHECKERS(MachineState, MachineClass, \
                     MACHINE, TYPE_MACHINE)

 

2단계

#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
    DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
    \
    DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)

대입하면

DECLARE_OBJ_CHECKERS(MachineState, MachineClass, \
                     MACHINE, TYPE_MACHINE)

중의 실제 값: InstanceType <-> MachineState, ClassType <-> MachineClass, OBJ_NAME <-> MACHINE, TYPENAME <-> TYPE_MACHINE, 그러면

DECLARE_INSTANCE_CHECKER(MachineState, MACHINE, TYPE_MACHINE)

DECLARE_CLASS_CHECKERS(MachineClass, MACHINE, TYPE_MACHINE)

 

3단계

#define DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
    static inline G_GNUC_UNUSED InstanceType * \
    OBJ_NAME(const void *obj) \
    { return OBJECT_CHECK(InstanceType, obj, TYPENAME); }

및

#define DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME) \
    static inline G_GNUC_UNUSED ClassType * \
    OBJ_NAME##_GET_CLASS(const void *obj) \
    { return OBJECT_GET_CLASS(ClassType, obj, TYPENAME); } \
    \
    static inline G_GNUC_UNUSED ClassType * \
    OBJ_NAME##_CLASS(const void *klass) \
    { return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME); }

각각 대입

DECLARE_INSTANCE_CHECKER(MachineState, MACHINE, TYPE_MACHINE)

DECLARE_CLASS_CHECKERS(MachineClass, MACHINE, TYPE_MACHINE)

중의 실제 값을 얻으면

매크로 매개변수 대입되는 실제 값
InstanceType MachineState (인스턴스 = 실제 객체 타입)
ClassType MachineClass (클래스 = 설계도 타입)
OBJ_NAME (= MODULE_OBJ_NAME) MACHINE
TYPENAME (= TYPE_##OBJ_NAME) TYPE_MACHINE (자동 생성)
static inline G_GNUC_UNUSED MachineState *
MACHINE(const void *obj)
{ return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE); }
static inline G_GNUC_UNUSED MachineClass *
MACHINE_GET_CLASS(const void *obj)
{ return OBJECT_GET_CLASS(MachineClass, obj, TYPE_MACHINE); }

static inline G_GNUC_UNUSED MachineClass *
MACHINE_CLASS(const void *klass)
{ return OBJECT_CLASS_CHECK(MachineClass, klass, TYPE_MACHINE); }

 

종합하면 다음을 얻는다.

typedef struct MachineState MachineState;
typedef struct MachineClass MachineClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(MachineState, object_unref)

static inline G_GNUC_UNUSED MachineState *
MACHINE(const void *obj)
{ return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE); }

static inline G_GNUC_UNUSED MachineClass *
MACHINE_GET_CLASS(const void *obj)
{ return OBJECT_GET_CLASS(MachineClass, obj, TYPE_MACHINE); }

static inline G_GNUC_UNUSED MachineClass *
MACHINE_CLASS(const void *klass)
{ return OBJECT_CLASS_CHECK(MachineClass, klass, TYPE_MACHINE); }

 

 

다시 TYPE_MACHINE 매크로의 정의를 (include/hw/boards.h에 있음) 대입하면

#define TYPE_MACHINE "machine"

얻는 결과는 다음과 같다.

typedef struct MachineState MachineState;
typedef struct MachineClass MachineClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(MachineState, object_unref)

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return OBJECT_CHECK(MachineState, obj, "machine");
}

static inline G_GNUC_UNUSED MachineClass *MACHINE_GET_CLASS(const void *obj)
{
    return OBJECT_GET_CLASS(MachineClass, obj, "machine");
}

static inline G_GNUC_UNUSED MachineClass *MACHINE_CLASS(const void *klass)
{
    return OBJECT_CLASS_CHECK(MachineClass, klass, "machine");
}

 

마침내 MACHINE 함수의 진짜 모습을 확인할수있다.

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE);
}

 

5. MACHINE 함수 심층 분석: OBJECT_CHECK와 object_dynamic_cast_assert

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE);
}

 

OBJECT_CHECK 매크로

/**
 * OBJECT_CHECK:
 * @type: The C type to use for the return value.
 * @obj: A derivative of @type to cast.
 * @name: The QOM typename of @type
 *
 * A type safe version of @object_dynamic_cast_assert.  Typically each class
 * will define a macro based on this type to perform type safe dynamic_casts to
 * this object type.
 *
 * If an invalid object is passed to this function, a run time assert will be
 * generated.
 */
#define OBJECT_CHECK(type, obj, name) \
    ((type *)object_dynamic_cast_assert(OBJECT(obj), (name), \
                                        __FILE__, __LINE__, __func__))

따라서 매크로 정의를 대입하면, MACHINE 함수의 최종 코드는 다음과 같다.

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return ((MachineState*)object_dynamic_cast_assert(OBJECT(obj), ("machine"), __FILE__, __LINE__, __func__));
}

 

object_dynamic_cast_assert 함수

Object *object_dynamic_cast_assert(Object *obj,
                                   const char *typename,
                                   const char *file, int line, const char *func)
{
    trace_object_dynamic_cast_assert(obj ? obj->class->type->name : "(null)",
                                     typename, file, line, func);

#ifdef CONFIG_QOM_CAST_DEBUG
    int i;
    Object *inst;

    for (i = 0; obj && i < OBJECT_CLASS_CAST_CACHE; i++) {
        if (qatomic_read(&obj->class->object_cast_cache[i]) == typename) {
            goto out;
        }
    }

    inst = object_dynamic_cast(obj, typename);

    if (!inst && obj) {
        fprintf(stderr, "%s:%d:%s: Object %p is not an instance of type %s\n",
                file, line, func, obj, typename);
        abort();
    }

    assert(obj == inst);

    if (obj && obj == inst) {
        for (i = 0; i < OBJECT_CLASS_CAST_CACHE; i++) {
            qatomic_set(&obj->class->object_cast_cache[i - 1],
                        qatomic_read(&obj->class->object_cast_cache[i]));
        }
        qatomic_set(&obj->class->object_cast_cache[i - 1], typename);
    }

out:
#endif
    return obj;
}

 

Object 구조체

typedef struct Object Object;

 

/**
 * struct Object:
 *
 * The base for all objects.  The first member of this object is a pointer to
 * a #ObjectClass.  Since C guarantees that the first member of a structure
 * always begins at byte 0 of that structure, as long as any sub-object places
 * its parent as the first member, we can cast directly to a #Object.
 *
 * As a result, #Object contains a reference to the objects type as its
 * first member.  This allows identification of the real type of the object at
 * run time.
 */
struct Object
{
    /* private: */
    ObjectClass *class;
    ObjectFree *free;
    GHashTable *properties;
    uint32_t ref;
    Object *parent;
};

실제로는 클래스 타입 검사를 수행하여, 전달된 객체 타입이 machine 타입인지 확인하는 것이다.

 

MACHINE 매크로 흐름

 

6. MACHINE의 실제 호출: object_new_with_class와 object_new

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE);
}

그리고 OBJECT_CHECK 매크로를 펼친 후의 함수 코드

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return ((MachineState*)object_dynamic_cast_assert(OBJECT(obj), ("machine"), __FILE__, __LINE__, __func__));
}

 

이제 다시 돌아가서, qemu_create_machine 함수(softmmu/vl.c)의 실제 호출을 살펴보자.

current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));

옛 버전 QEMU 코드에서 이 한 줄은 이렇다.

current_machine = MACHINE(object_new(object_class_get_name(OBJECT_CLASS(machine_class))));

 

이전에는 MACHINE 함수가 중점이었는데, 지금은 그것의 매개변수인 object_new_with_class 및 object_new 함수를 중점적으로 보자.

Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);

    return object_new_with_type(ti);
}
Object *object_new_with_class(ObjectClass *klass)
{
    return object_new_with_type(klass->type);
}

보다시피, 어느 함수든 결국에는 object_new_with_type 함수를 호출한다.

 

신버전에서 object_new_with_class에 넘기는 machine_class는 새로 만든 게 아니라, qemu_create_machine 첫 단계에서 (select_machine 등으로) 이미 구해둔 그 MachineClass다.

qemu_create_machine() 첫 단계
   → select_machine() 등으로 machine_class 획득   ← "다음 함수로 얻은 것"
   → (그 machine_class를 들고 있다가)
   → object_new_with_class(OBJECT_CLASS(machine_class))  ← 그걸로 실제 MachineState 객체 생성
MachineClass *machine_class = select_machine(qdict, &error_fatal);
/**
 * OBJECT_CLASS:
 * @class: A derivative of #ObjectClass.
 *
 * Converts a class to an #ObjectClass.  Since all objects are #Objects,
 * this function will always succeed.
 */
#define OBJECT_CLASS(class) \
    ((ObjectClass *)(class))
typedef struct ObjectClass ObjectClass;
/**
 * struct ObjectClass:
 *
 * The base for all classes.  The only thing that #ObjectClass contains is an
 * integer type handle.
 */
struct ObjectClass
{
    /* private: */
    Type type;
    GSList *interfaces;

    const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
    const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];

    ObjectUnparent *unparent;

    GHashTable *properties;
};

최종적으로 사용되는 것은 ObjectClass 안의 Type type 멤버다.

 

구버전의 경우

호출하는 것은 object_new(object_class_get_name(OBJECT_CLASS(machine_class)))이다.

const char *object_class_get_name(ObjectClass *klass)
{
    return klass->type->name;
}

 

ObjectClass 구조 중 Type 타입의 정의를 보자

struct TypeImpl;
typedef struct TypeImpl *Type;

여기서 알 수 있듯이, 실제로 Type과 TypeImpl은 본질적으로 같은 것을 가리킨다.

TypeImpl은 struct TypeImpl이고, Type은 struct TypeImpl *이다.

object_new 함수에서는 type_get_by_name 함수도 호출한다.

static TypeImpl *type_get_by_name(const char *name)
{
    if (name == NULL) {
        return NULL;
    }

    return type_table_lookup(name);
}

 

static TypeImpl *type_table_lookup(const char *name)
{
    return g_hash_table_lookup(type_table_get(), name);
}

 

정리하자면

신버전에서 호출하는 것은 object_new_with_class(OBJECT_CLASS(machine_class))이다. 한 겹씩 펼치면 다음과 같다

object_new_with_class(OBJECT_CLASS(machine_class)) --->
object_new_with_class(((ObjectClass *)(machine_class))) --->
object_new_with_type(((ObjectClass *)(machine_class))->type)

 

구 버전에서 호출하는 것은 object_new(object_class_get_name(OBJECT_CLASS(machine_class)))이다. 한 겹씩 펼치면 다음과 같다.

object_new(object_class_get_name(OBJECT_CLASS(machine_class))) --->
object_new(object_class_get_name(((ObjectClass *)(machine_class)))) --->
object_new(((ObjectClass *)(machine_class))->type->name) --->
object_new_with_type(type_get_by_name(((ObjectClass *)(machine_class))->type->name))

두 가지의 마지막(한 줄) 코드를 비교해보면 알 수 있듯이, 신버전이 구 버전보다 한 단계가 적다.

명백히 ObjectClass->type을 직접 얻을 수 있는데, 구버전은 굳이 ObjectClass->type->name을 통해 다시 type_get_by_name으로 ObjectClass->type을 되짚어 찾아간다.

 

 

current_machine과 MachineState

MACHINE 함수는 최종적으로 새로 만든 MachineState 객체를 current_machine에 반환할 것이다.

current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));
MachineState *current_machine;
OBJECT_DECLARE_TYPE(MachineState, MachineClass, MACHINE)

OBJECT_DECLARE_TYPE 매크로는 여기서 더 이상 펼치지 않겠다. MachineState가 struct MachineState로부터 온다는 것만 알면 된다.

typedef struct MachineState MachineState;
/**
 * MachineState:
 */
struct MachineState {
    /*< private >*/
    Object parent_obj;

    /*< public >*/

    void *fdt;
    char *dtb;
    char *dumpdtb;
    int phandle_start;
    char *dt_compatible;
    bool dump_guest_core;
    bool mem_merge;
    bool usb;
    bool usb_disabled;
    char *firmware;
    bool iommu;
    bool suppress_vmdesc;
    bool enable_graphics;
    ConfidentialGuestSupport *cgs;
    HostMemoryBackend *memdev;
    /*
     * convenience alias to ram_memdev_id backend memory region
     * or to numa container memory region
     */
    MemoryRegion *ram;
    DeviceMemoryState *device_memory;

    ram_addr_t ram_size;
    ram_addr_t maxram_size;
    uint64_t   ram_slots;
    BootConfiguration boot_config;
    char *kernel_filename;
    char *kernel_cmdline;
    char *initrd_filename;
    const char *cpu_type;
    AccelState *accelerator;
    CPUArchIdList *possible_cpus;
    CpuTopology smp;
    struct NVDIMMState *nvdimms_state;
    struct NumaState *numa_state;
};

 

MachineState 구조 시각화

여기까지 Machine 관련 객체가 비로소 생성 완료되었다. 이어서 많은 디바이스(CPU, 메모리 등)의 초기화는 모두 Machine 객체를 중심으로 진행될 것이며, 뒤에서는 current_machine을 자주 보게 될 것이다.

 

전체 호출 시퀀스

 

핵심 데이터 흐름

 

매크로/함수 책임 분담 요약

'System Programming > Hypervisor' 카테고리의 다른 글

QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조  (0) 2025.02.14
QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트  (0) 2024.11.18
QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화  (0) 2024.07.08
QEMU 코드 분석 #2 KVM CPU가상화  (0) 2024.07.08
QEMU 코드 분석 #1 파라미터 해석 · 모듈 초기화 · QOM · 속성  (0) 2024.03.25
    'System Programming/Hypervisor' 카테고리의 다른 글
    • QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조
    • QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트
    • QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화
    • QEMU 코드 분석 #2 KVM CPU가상화
    1000sj
    1000sj

    티스토리툴바