AirBuild
엔지니어링

최초의 App Store, AI 코딩으로 새롭게 그리다.

2008년, iPhone 3G와 최초의 App Store는 많은 사람에게 모바일 앱을 처음 소개했습니다. 우리는 그 휴대폰에 새로운 쓰임을 더하려고 AirBuild3G를 만들었습니다. iPhone 3G 또는 3GS에서 새 앱을 설명하고, 코딩 모델과 함께 소스 코드를 다듬고, 같은 휴대폰에서 컴파일하고 실행할 수 있습니다. 모델은 사용자가 선택한 서비스에서 실행됩니다. 프로젝트 파일과 개발 도구는 휴대폰에 남습니다.

AirBuild3G 자체는 Mac에서 Xcode 27로 빌드했습니다. AirBuild3G로 만드는 앱은 휴대폰에 설치된 별도의 컴파일러를 사용합니다. 둘 다 작동하게 하려면 기기 준비, 링크, HTTPS, 휴대폰 자체 툴체인이라는 네 영역을 손봐야 했습니다. 아래에서는 첫 앱을 만들기까지 거친 작업을 차례로 설명합니다.

원래 하드웨어 준비하기

작은 디스플레이와 물리적인 홈 버튼을 보면 iPhone 3G임을 바로 알 수 있습니다. 이 기기를 위해 다시 앱을 만든다는 것은 익숙한 앱의 느낌을 만들던 컨트롤까지, 원래 비율과 한계를 존중한다는 뜻입니다. AirBuild3G 자체는 그룹형 설정 표와 탐색 막대 동작을 비롯해 iOS 4에서 사용할 수 있는 UIKit 컨트롤을 사용합니다.

AirBuild3G는 iPhone 3G와 3GS에서 실행됩니다. 3G에는 armv6 바이너리가 필요하고, 3GS는 armv7을 사용합니다. 각 휴대폰에 설치할 수 있는 환경 패키지는 펌웨어에 따라 결정됩니다. 기준으로 삼은 armv6 기기는 커스텀 펌웨어가 설치된 iOS 4.2.1 실행 iPhone 3G입니다.

  1. 준비복원 / 활성화
  2. 연결SSH + 네트워크
  3. 설치앱 + 환경 패키지
  4. 환경설정 → 환경
설정은 네 단계입니다. 휴대폰을 준비하고 연결한 뒤 AirBuild3G와 환경 패키지를 설치하고, 설정 → 환경에서 설치를 탭합니다.

홈 화면으로 부팅되고 Mac의 SSH 연결을 허용하는 휴대폰부터 준비합니다. Debian 패키지를 받고 설치하려면 작동하는 네트워크 연결과 패키지 관리자가 필요합니다. 보낼 패키지는 하드웨어 모델과 펌웨어 버전으로 결정하고, 보낼 위치는 네트워크 주소로 결정합니다. 안정적인 USB 케이블과 전원도 필요합니다.

일부 구형 휴대폰은 활성화와 시스템 접근을 위한 추가 단계가 필요합니다. 지원되는 하드웨어라면 Legacy iOS Kit을 사용하고 해당 휴대폰 모델과 펌웨어에 맞는 안내를 따르세요. 시작하기 전에 휴대폰을 백업하세요. AirBuild3G 자체는 휴대폰을 활성화하지 않습니다.

AirBuild3G의 TLS 지원에는 Substrate도 필요합니다. 3G용 빌드에는 armv6가 포함되어야 합니다. 버전과 관계없이 armv7 전용 패키지는 3G에서 실행되지 않습니다. Legacy iOS Kit으로 준비한 휴대폰에는 호환되는 빌드가 이미 있을 수 있습니다. Substrate가 작동한다면 그대로 두세요.

설정 → 환경에도 같은 원칙이 적용됩니다. 개발 도구를 추가하되, 작동하는 패키지 관리자와 SSH 서비스 등 기본 구성 요소는 그대로 둡니다. 이런 구성 요소를 한꺼번에 업그레이드하면 구형 시스템에서 실행할 수 없는 바이너리가 설치될 수 있습니다.

Xcode 27로 빌드하기

Xcode 27의 컴파일러는 여전히 armv6와 armv7 코드를 생성하고 iOS 4.0 배포 대상을 허용합니다. Xcode 자체에는 iOS 27 SDK만 포함되어 있으므로 레거시 SDK의 헤더를 기준으로 컴파일합니다. make 명령줄에서 SDK 경로를 LEGACY_SDK로 전달하거나 내보내세요. 환경 패키지를 빌드하는 스크립트도 이 값을 읽으므로 Makefile 안에서만 설정한 값은 스크립트에 전달되지 않습니다.

링커의 제약은 더 엄격합니다. armv6를 링크 대상으로 거부하고, iOS 4.3 미만의 배포 대상을 허용하지 않으며, 레거시 SDK의 라이브러리가 알 수 없는 플랫폼용으로 빌드되었다고 보고합니다. 이 라이브러리들은 생성한 스텁을 대상으로 링크하고, 레거시 SDK는 헤더에만 사용합니다.

  1. 컴파일clang
    아키텍처
    armv6
    최소 iOS
    4.0
    코드
    e92d40f0 …
  2. 링크레이블 변경 후 ld
    아키텍처
    armv6armv7
    최소 iOS
    4.05.0
    코드
    e92d40f0 …
  3. 배포레이블 복원 후 패치
    아키텍처
    armv7armv6
    최소 iOS
    5.04.0
    코드
    e92d40f0 …
    make verify
Clang이 armv6 명령어를 생성합니다. 빌드는 링크 전후에 메타데이터만 다시 쓴 다음 결과를 검사합니다.

빌드는 두 슬라이스를 iOS 4.0용으로 컴파일하고 iOS 5.0용으로 링크한 다음, 최소 버전을 다시 4.0으로 패치합니다. armv6의 경우 링커가 받아들이도록 모든 오브젝트 파일의 레이블을 armv7로 바꾸고, 링크된 슬라이스의 레이블을 다시 armv6로 되돌립니다. 레이블 변경은 명령어가 아니라 메타데이터를 바꿉니다. 컴파일된 코드와 링커가 추가하는 심볼 스텁은 처음부터 armv6에서 실행될 수 있어야 합니다.

tools/relabel_arch.py
MH_MAGIC = 0xFEEDFACE
SUBTYPES = {"armv6": 6, "armv7": 9}

def main():
    …
    path, want = sys.argv[1], SUBTYPES[sys.argv[2]]

    with open(path, "rb") as f:
        buf = bytearray(f.read())

    if struct.unpack_from("<I", buf, 0)[0] != MH_MAGIC:
        …

    struct.pack_into("<i", buf, 8, want)
    with open(path, "wb") as f:
        f.write(buf)
  1. 각 아키텍처의 Mach-O cpusubtype: 6은 armv6, 9는 armv7입니다.
  2. 레이블 변경의 전부는 오프셋 8에 있는 네 바이트, 즉 헤더의 cpusubtype 필드입니다. 명령어는 전혀 바뀌지 않습니다.
빌드는 링크 전에 모든 armv6 오브젝트에 이 스크립트를 실행하고, 링크 후에는 링크된 슬라이스에 실행합니다.

iOS 4.3 이전에는 ASLR이 없었으므로 빌드는 -no_pie를 유지합니다. Objective-C 카테고리 병합도 끕니다. 켜 두면 병합된 메서드에서 Thumb 비트가 제거되어 3GS에서 AirBuild3G가 충돌하기 때문입니다. make verify는 아키텍처, 로드 명령, armv6 명령어와 armv7 메서드의 Thumb 비트를 검사합니다. 링크에 성공했다는 사실만으로는 어느 것도 확인할 수 없습니다.

make를 실행해 AirBuild3G와 권한이 있는 헬퍼를 빌드합니다. make payload는 버전이 고정된 모든 패키지를 로컬 캐시에 다운로드하고 검증하며, make package는 이를 사용해 오프라인에서 AirBuild3G 패키지와 두 환경 패키지를 빌드합니다. 각 환경 패키지에 휴대폰용 SDK 아카이브가 들어가므로 패키징에도 LEGACY_SDK가 필요합니다.

make install-device DEVICE=root@<ip>는 AirBuild3G와 iOS 4 환경 패키지를 설치하고 홈 화면을 새로 고친 다음 앱을 다시 시작합니다. iOS 5 이상을 실행하는 3GS라면 대신 tools/deploy_device.sh의 세 번째 인수로 ios6를 전달하세요. 앱은 mobile 사용자로 실행되고, 옆에 있는 작은 헬퍼가 root 권한이 필요한 모든 작업을 실행합니다. 이제 환경 패키지가 휴대폰에 있습니다. 설정 → 환경에서 패키지에 포함된 개발 도구를 설치합니다.

모델 서비스에 연결하기

설정의 엔드포인트 아래에 기본 URL, API 키, 모델이라는 세 값을 입력합니다. 요청은 휴대폰에서 해당 엔드포인트로 직접 전송됩니다. 중간에서 실행하는 서버가 없고 계정이나 기기 등록도 없습니다. 모델 서비스가 모델을 실행하고 사용량을 청구하며 입력할 값을 안내합니다. 휴대폰을 저희에게 선물받았다면 대신 air.build에 연결됩니다.

기본 iOS 4는 SecureTransport를 통해 TLS 1.0만 지원하지만, 최신 엔드포인트에는 TLS 1.2가 필요할 수 있습니다. CFNetwork용 Substrate 트윅인 TLSFix로 TLS 1.2 지원을 추가합니다.

휴대폰은 여전히 서버의 신원을 확인해야 합니다. 오래된 신뢰 저장소는 최신 엔드포인트가 사용하는 인증서 체인을 항상 구성할 수 있는 것은 아니며, TLS 1.2를 추가해도 이 저장소는 업데이트되지 않습니다. 연결에 실패하면 암호화와 인증서 신뢰를 별개의 문제로 다루세요.

AirBuild3G는 기본적으로 인증서를 검증합니다. 요청에 실패하면 먼저 기본 URL, API 키, 모델과 인증서 체인을 확인하세요. 모든 인증서 신뢰 설정은 명시적인 예외입니다. 이 설정을 켜면 서버 신원 확인 절차가 사라져, 엔드포인트를 사칭할 수 있는 사람에게 API 키가 노출될 수 있습니다. 엔드포인트가 자체 네트워크에 있지 않다면 이 설정을 끈 채로 두세요. AirBuild3G는 신뢰를 평가할 때마다 설정을 읽으므로, 설정을 끄면 다음 요청부터 적용됩니다.

src/ABLTrustAll.m
static OSStatus ABLSecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result) {
	…
	OSStatus status = ABLOriginalSecTrustEvaluate(trust, result);
	if (!ABLTrustAnyCertificate()) {
		return status;
	}
	if (result != NULL) {
		*result = kSecTrustResultUnspecified;
	}
	return errSecSuccess;
}
  1. 먼저 휴대폰 자체 평가가 실행됩니다.
  2. 모든 인증서 신뢰는 기본적으로 꺼져 있으므로 그 결과를 변경하지 않고 반환합니다. 호출할 때마다 NSUserDefaults에서 설정을 읽습니다.
SecTrustEvaluate의 훅은 모든 인증서 신뢰가 켜져 있지 않으면 휴대폰 자체의 평가 결과를 반환합니다.

스트리밍 응답도 휴대폰의 한계에 맞아야 합니다. AirBuild3G는 백그라운드 스레드에서 스트림을 받아 디코딩하고, 완성된 텍스트만 인터페이스에 전달합니다. 모든 토큰마다 다시 측정하고 스크롤하는 대신 일정한 간격으로 대화 내용을 새로 고쳐, 응답이 도착하는 동안 레이아웃 작업을 줄입니다. 응답이 표시되면 연결이 작동한다는 뜻입니다. 모델이 작성한 내용을 컴파일하려면 휴대폰에 환경이 설치되어 있어야 합니다.

휴대폰에서 실행할 수 있는 컴파일러 설치하기

설정 → 환경은 환경 패키지에서 컴파일러, SDK 헤더, 링크 라이브러리와 서명 도구를 설치합니다. iOS 4 패키지에는 armv6용 GCC가 들어 있고, iOS 5 이상용 패키지에는 armv7용 clang이 들어 있습니다. 각 휴대폰은 펌웨어에 맞는 패키지만 설치합니다.

모든 패키지는 다운로드 주소와 체크섬으로 고정되어 있으며, 설치할 때 저장소에 최신 버전을 요청하지 않고 지정된 파일을 그대로 사용합니다. 고정된 버전이 이미 설치된 패키지는 건너뛰므로 환경 설치를 두 번째로 실행하면 곧바로 끝납니다. 설치된 패키지는 이후 보류 상태로 설정되어 Cydia가 툴체인을 망가뜨리는 업그레이드를 제안하지 않게 됩니다.

iPhone 3G용 환경은 2008 빌드 iphone-gcc 4.2를 사용합니다. 2009 빌드는 미리 컴파일된 헤더에 1기가바이트를 예약하며, 그 일부가 armv6 휴대폰의 주소 공간을 벗어납니다. 컴파일러 백엔드는 어떤 코드도 컴파일하기 전에 충돌합니다. 두 빌드 모두 Apple 빌드 5555로 표시되지만 이 주소 공간에 들어가는 것은 2008 빌드뿐입니다.

armv6 한계0x30000000

2008 iphone-gcc · 모든 세그먼트가 6 MB 미만실행됨

512 MB 배열 · 테스트 바이너리실행됨

768 MB 배열 · 테스트 바이너리SIGBUS

2009 iphone-gcc · 1 GB 미리 컴파일된 헤더 영역SIGBUS

2008 컴파일러는 armv6 한계 안에 들어갑니다. 2009 빌드는 예약 영역이 한계를 넘기 때문에 main이 실행되기 전에 실패합니다. 512 MB와 768 MB 테스트 배열은 각각 한계의 안쪽과 바깥쪽에 놓입니다.

SDK 헤더도 수정해야 합니다. 이 GCC는 헤더의 블록 선언을 파싱할 수 없습니다. iOS 4 헤더를 패키징하기 전에 이 선언들과 제거된 타입에 의존하는 모든 선언을 삭제합니다. clang은 블록을 지원하므로 clang 패키지는 Apple의 헤더를 변경하지 않습니다. 각 패키지에는 해당 컴파일러가 생성한 결과물에 서명할 수 있는 서명 도구도 포함됩니다.

휴대폰은 시스템 프레임워크를 dyld 공유 캐시에 보관하므로, 프레임워크 디렉터리에는 링커가 읽을 라이브러리 파일이 없습니다. 우리는 내보낸 심볼과 실제 라이브러리 이름이 담긴 생성 스텁을 제공합니다. 링커는 각 참조를 스텁에서 확인하고 어느 라이브러리에서 왔는지 기록합니다. 실행 시 dyld는 이를 공유 캐시의 실제 구현에 바인딩합니다. 스텁에 실제 심볼 이름이 들어 있으므로 프레임워크를 빠뜨리면 앱 실행 중 충돌하는 대신 링크 단계에서 실패합니다.

환경은 include 경로도 설정하고, gcc와 cc가 설치된 컴파일러를 가리키게 하며, Objective-C 런타임과 CoreFoundation을 가져오는 링크 플래그를 내보냅니다. 번들 Makefile이 이 설정을 읽으므로 새 프로젝트에서 별도로 툴체인을 설정할 필요가 없습니다. 이후 앱은 휴대폰 자체에서 빌드됩니다.

template/Makefile
build/%.o: src/%.m | build
	@echo "  compile $<"
	@gcc -c $(IOS_CFLAGS) $< -o $@

# Compiling and linking take different flags, so they are separate steps.
# A missing -framework is a link error here, which is the point: link stubs
# with real symbol names are what make it one instead of a crash at launch.
build/$(NAME): $(OBJECTS)
	@echo "  link    $@"
	@gcc $(OBJECTS) -o $@ $(IOS_LDFLAGS) $(addprefix -framework ,$(FRAMEWORKS))
	@# Unsigned is SIGKILL at launch, not a warning.
	@ldid -S $@
  1. IOS_CFLAGS와 IOS_LDFLAGS는 환경이 설치하는 /etc/profile.d/airbuild.sh에서 가져오므로 어느 프로젝트도 SDK나 컴파일러 경로를 지정하지 않습니다.
  2. 바이너리 서명도 휴대폰에서 이루어집니다. 서명되지 않은 바이너리는 실행과 동시에 종료됩니다.
새 프로젝트가 시작할 때 사용하는 Makefile입니다. 휴대폰은 설치된 툴체인만으로 컴파일하고 링크하고 서명합니다.

첫 앱 만들고 실행하기

새 프로젝트는 번들 UIKit 템플릿에서 시작합니다. AirBuild3G는 템플릿을 프로젝트 디렉터리에 복사하고 파일을 모델의 컨텍스트에 넣습니다. 모델은 이미 빌드되는 프로젝트에서 시작하므로, 첫 요청에서 Makefile을 만들어 달라고 하지 않고 앱을 설명할 수 있습니다.

첫 앱으로는 화면의 숫자를 올리는 버튼이 좋습니다. 전체 코드를 읽을 만큼 작으면서 휴대폰에서 편집, 컴파일, 패키징과 실행까지 모두 시험할 수 있습니다. 요청에서 iOS 4를 대상으로 지정하고 모델에 앱을 빌드하고 설치해 달라고 하세요. 인터페이스 코드는 휴대폰의 iOS가 구현하는 API만 사용해야 합니다. 헤더에 선언이 있다고 해서 해당 API가 실행 시점에 존재하는 것은 아닙니다.

모델에는 네 가지 도구가 있습니다. Execute는 프로젝트 디렉터리에서 명령을 실행합니다. EditFile은 파일 전체를 씁니다. PatchFile은 정확히 일치하는 구간을 바꿉니다. CheckBuild는 환경 설정을 불러온 상태로 make를 실행하고 출력의 마지막 부분을 반환합니다. 도구 결과는 모델로 돌아가며, 모델은 작업을 이어가거나 실패한 변경을 수정할 수 있습니다. 추론은 모든 과정에서 사용자의 모델 서비스에서 이루어집니다.

원격모델사용자의 엔드포인트

도구 호출

도구 결과

휴대폰에서도구Execute EditFile PatchFile CheckBuild

모델이 도구 호출 없이 응답하거나 사용자가 중지를 탭하거나 16회가 실행될 때까지 루프가 반복됩니다.

한 번의 도구 실행 과정입니다. 모델은 원격에서 실행되고 도구는 휴대폰에서 실행됩니다. 루프가 계속되면 각 결과가 다음 요청에 들어갑니다.

새 앱이 설치되면 휴대폰에서 여세요. 카운터 앱이라면 버튼을 탭하고 숫자가 바뀌는 모습을 보면서 요청한 대로 작동하는지 정확히 확인할 수 있습니다. 이후 요청에서 레이아웃을 바꾸거나 다른 상호작용을 추가할 수 있고, 모델은 기존 파일과 앞선 도구 결과를 바탕으로 작업합니다.

AirBuild3G는 각 프로젝트와 대화를 휴대폰에 보관하며, 도구를 한 번 실행할 때마다 저장합니다. 앱을 나가거나 메모리가 부족하면 프로세스가 종료될 수 있으므로 열려 있던 프로젝트도 기억합니다. AirBuild3G를 다시 열면 대화와 파일이 있는 해당 프로젝트로 돌아갑니다. 다음 메시지는 저장된 대화에서 이어지고, 소스도 그대로 남아 다음 편집과 빌드에 사용할 수 있습니다.

이 화면에서는 그 카운터 하나만으로도 예전에 사용하던 앱과 지금 만들 수 있는 앱이 이어집니다.

익숙한 화면에 새로운 것

iPhone 3G는 2008년, App Store가 문을 열던 때 등장했습니다. 새 앱 하나로 어디든 들고 다니는 악기나 공책, 게임이 될 수 있었습니다. 홈 버튼을 누르면 아이콘이 늘어선 화면으로 돌아갔고, 각 아이콘은 다른 무언가를 써 보라는 초대장이었습니다.

오늘 그 휴대폰을 만지면 2008년이 얼마나 오래전이었는지 느껴집니다. 디스플레이는 커지지 않았습니다. 프로세서도 빨라지지 않았습니다. 원래 인터페이스는 여전히 몇 가지 컨트롤과 작은 공간을 세심하게 사용하라고 요구합니다. 이런 한계 안에서 작업하면 최신 기기에서는 지나치기 쉬운 세부 사항이 눈에 들어옵니다. 버튼이 반응하는지, 대화가 매끄럽게 스크롤되는지, 다음 화면이 제때 나타나는지 살피게 됩니다.

AirBuild3G는 익숙한 화면에 한 가지 역할을 더합니다. 최신 모델이 앱 작성을 돕고, 휴대폰과 같은 시대의 컴파일러가 앱을 빌드합니다. 오래된 아이콘은 그 자리에 그대로 둘 수 있습니다.

그 옆에는 전에 없던 앱이 들어갈 자리도 있습니다.

GitHub의 AirBuild3G

AirBuild 팀이 사랑을 담아 만들었습니다