태그 보관물: Linux

Linux에서 메모리 포인터의 유효성 검증

Windows에서와 달리 Linux환경에서는 딱히 포인터의 유효성을 검증할 수 있는 system call이 없다. 이 포스팅은 Linux환경에서 이와 유사한 기능을 구현하기 위해 “정보의 바다”에서 찾은 내용들을 정리해 둔 것이다.

1. _etext를 이용하는 방법

첫번째 방법은 Define and use a pointer validation function 이라는 위키문서에서 가져온 것인데 컴파일러가 생성하는 text 영역의 시작점을 이용해서 포인터 값이 이를 침범 하는지 여부를 검사하고 유효성을 판단한다. 하지만 위 링크의 커멘트에도 나와 있듯이 시스템에 따라 동작하지 않을 수도 있으므로 reliable 한 구현이라 볼 수는 없다.

bool isValidPointer_ET(void *ptr) {
    extern const char _etext;
    return (ptr != nullptr) && ((const char*)ptr > &_etext);
}

2. msync()를 이용하는 방법

Checking whether a pointer is valid in Linux라는 블로그 포스트에 소개된 방법으로 매핑된 메모리 공간을 동기화 할 때 쓰는 msync() 시스템 콜을 호출 하면서 유효하지 않은 page 시작 주소를 넘겨 주면 0이 아닌 음수 값을 반환하는 것을 이용하는 방법이다. (이 경우 errno에 ENOMEM이 설정된다)

#include <sys/mman.h>
#include <unistd.h>

bool isValidPointer_MS(void *ptr) {
    const size_t pageSize = sysconf(_SC_PAGESIZE);
    void *basePtr = (void *)((((size_t)ptr) / pageSize) * pageSize);
    return msync(basePtr, pageSize, MS_SYNC) == 0;
}

3. mincore()를 이용하는 방법

Stackoverflow에 올려진 Testing pointers for validity (C/C++)라는 질문에 대한 답변에 있는 아이디어 중 하나인데 메모리 페이지의 swap 상태를 확인해서 반환해 주는 mincore()을 이용하는 방법이다. 해당 답변에는 다른 아이디어 들도 있으니 필요에 따라 참고.

#include <sys/mman.h>
#include <unistd.h>

bool isValidPointer_MC(void *ptr) {
    unsigned char vec = 0;
    const size_t pageSize = sysconf(_SC_PAGESIZE);
    void *basePtr = (void *)((((size_t)ptr) / pageSize) * pageSize);
    int ret = mincore(basePtr, pageSize, &vec);
    return (ret == 0 && ((vec & 0x1) == 0x1));
}

시험 결과와 결론

위의 함수들에 대해 Linux환경에서 전역 변수 포인터, 지역 변수 포인터, 널 포인터, 널 포인터는 아니지만 명백하게 무효한 포인터(0x04 같은), 동적 할당된 공간에 대한 유효성 여부는 잘 동작한다.

하지만, 이미 해제된 포인터나 할당되지 않은 heap 공간 내의 임의 주소에 대해서는 제대로 유효성 여부를 판단하지 못하고, 주소 범위가 유효 하다면 포인터 역시 유효 하다고 판단하는 오류가 세가지 구현 모두에 있다.

    // 해제된 heap공간에 대한 유효성 여부 확인. 모두 실패함.
    unsigned int* dynamicVar = new unsigned int[100];
    delete[] dynamicVar;
    EXPECT_FALSE(isValidPointer_ET(dynamicVar));
    EXPECT_FALSE(isValidPointer_MS(dynamicVar));
    EXPECT_FALSE(isValidPointer_MC(dynamicVar));


    // 할당 되지 않은 Heap공간 내의 임의 포인터에 대한 유효성 확인. 모두 실패함.
    unsigned int* dynamicUnallocVar = dynamicVar + 100;
    EXPECT_FALSE(isValidPointer_ET(dynamicUnallocVar));
    EXPECT_FALSE(isValidPointer_MS(dynamicUnallocVar));
    EXPECT_FALSE(isValidPointer_MC(dynamicUnallocVar)); 

즉, 위의 구현 들은 주어진 포인터가 유효한 메모리 공간내에 속하는지는 확인할 수 있어도, 동적 할당 영역의 메모리 포인터가 실제 read/write 가능한 상태인지 여부는 정확히 반환 할 수 없다.

시험에 사용한 code는 여기에 붙여 둔다.

GDB에서 인스턴스의 실제 타입 표시하기

Base class로 부터 상속 받은 Derived class의 인스턴스를 Base class 포인터에 넣으면 GDB에서 타입이 제대로 표시되지 않는다. 이 때는 ‘set print object on’을 설정해서 GDB의 ptype <var> 명령 결과에 해당 인스턴스의 실제 타입이 함께 표시 되도록 할 수 있다.

예를 들어 다음과 같은 코드가 있다고 할 때

#include <iostream>
using namespace std;

class Base {
public:
  virtual void identify() {
      cout << "Base class" << endl;
  }
};

class Derived: public Base {
public:
  void identify() override {
      cout << "Derived class" << endl;
  }
};


int main(void) {
  // Derived class instance 이지만
  // GDB에서 Base type으로 표시된다.
  Base *c = new Derived();
  c->identify();

  delete c;
}

main() 함수 내의 “Base *c”를 GDB에서 확인해 보면 실제 인스턴스의 타입과 관련 없이 Base class로 표시된다.

(gdb) ptype c
type = class Base {
  public:
    virtual void identify(void);
} *

실제 인스턴스가 무엇인지 알고 싶다면, set print ojbect on을 설정해서 ptype 결과에 실제 인스턴스가 함께 표시되도록 할 수 있다. ( /* real type = … */)

(gdb) set print object on
(gdb) ptype c
type = /* real type = Derived * */
class Base {
  public:
    virtual void identify(void);
} *

이 설정을 다른 유용한 것들과 함께 ~/.gdbinit에 넣어두면 gdb가 실행될 때 자동으로 설정된다.

$ cat ~/.gdbinit
set print object on
set print pretty on
set print static-members on
set print vtbl on

Gitlab test-runner의 FATAL: the “HOME” is not set 문제

GitLab Runner 문서를 보고 열심히 따라하고 설정까지 마쳤는데 러너가 동작하지 않고 commit을 하면 CI가 한 참 동안을 pending 상태에 있다가 실패했다는 에러 메일 보내 버리는 증상이 있다. Systemctl로 서비스 상태를 보니 제대로 올려지지 않고 ‘inactive’이거나 ‘activating’에 계속 머물러 있다. 하지만 만약 background 서비스로 동작시키지 않고 다음과 같이 run command로 foreground에서 돌리면 잘 동작한다. 환경 설정에는 ‘거의’ 문제가 없다는 뜻 이겠지..

$ sudo /usr/local/bin/gitlab-runner run

Journalctl로 서비스 로그를 보니 FATAL에러가 하나가 잡힌다.

$ journalctl -u gitlab-runner.service

...
gitlab-runner[4622]: FATAL: the "HOME" is not set                      
systemd[1]: gitlab-runner.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: gitlab-runner.service: Failed with result 'exit-code'.

오호라. HOME 환경 변수를 찾으려 했는데 없어서 죽은 거고만! 해결책은 간단히 HOME 환경 변수를 선언해 주면 된다. 서비스 설정파일 (/etc/systemd/system/gitlab-runner.service)를 열고 [Service] 항목에 환경 변수를 추가해 준다. 만약 proxy환경이고 환경변수로 설정하고 있다면 서비스에서는 환경변수를 접근할 수 없으므로 여기에 함께 선언해 준다.

[Unit]
Description=GitLab Runner
After=syslog.target network.target
ConditionFileIsExecutable=/usr/local/bin/gitlab-runner
 
[Service]
Environment=HOME=/home/gitlab-runner # 이 부분을 추가
# 해당하는 경우 proxy 설정
#Environment=no_proxy=노프록시 설정
#Environment=NO_PROXY=노프록시 설정
#Environment=http_proxy=HTTP프록시 주소
#Environment=HTTP_PROXY=HTTP프록시 주소
#Environment=https_proxy=HTTPS프록시 주소
#Environment=HTTPS_PROXY=HTTPS프록시 주소
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/gitlab-runner "run" "--working-directory" "/home/gitlab-runner" "--config" "/etc/gitlab-runner/config.toml" "--service" "gitlab-runner" \
  "--syslog" "--user" "gitlab-runner"

Restart=always
RestartSec=120

[Install]
WantedBy=multi-user.target

이제 서비스를 다시 로드하고 Gitlab runner를 재 시작한다.

$ sudo systemctl daemon-reload
$ sudo /usr/local/bin/gitlab-runner restart

Systemctl에서 상태를 확인한다. 물론 CI test도 함께.

$ sudo systemctl status gitlab-runner.service 
● gitlab-runner.service - GitLab Runner
   Loaded: loaded (/etc/systemd/system/gitlab-runner.service; enabled; vendor preset: disabled)
   Active: active (running) since xxx; 1min 55s ago
 Main PID: 12954 (gitlab-runner)
    Tasks: 9 (limit: 4915)
   Memory: 7.9M
   CGroup: /system.slice/gitlab-runner.service
           └─12954 /usr/local/bin/gitlab-runner run --working-directory /home/gitlab-runner --config /etc/gitlab-runner/config.toml --service gitlab-runner

Travis CI 설정과 docker image 사용

GitHub project에 CI를 붙이고 싶은데 Jenkins server가 회사 firewall 안에 들어 있어서 GitHub에서 직접 webhook을 붙일 수 없는 문제가 있다. Jenkins의 GitHub plugin으로 tunneling을 설정하는 방법 등 있기는 하지만 다른 CI 옵션들을 살펴 보던중 Open source project에 대해서는 무료라는 Travis CI가 있다는 것을 알게 되었다. Travis CI는 기본으로 Ubuntu를 지원하고 그 외의 경우는 docker를 사용해서 환경을 설정할 수도 있다. 이 포스팅은 Travis CI에서 ClearLinux docker를 사용한 설정에 대한 기록이다.

삽질1: Travis CI의 Ubuntu이용

빌드와 Google test를 이용한 unit test만 할 것이니까 OS를 크게 타지 않을테니 기본으로 제공되는 Ubuntu 환경에 필요한 도구들만 설치 하면 가장 빠르지 않을까?

일견 타당해 보이기는 하지만 문제는 의존성이다. Pre-compile된 Google test를 download 받는다 해도, 2019년 1월 현재 아직 Travis CI에서 제공하는 Ubuntu의 가장 최신 버전은 Xenial이다. CMake version이 안맞아서 최신버전으로 설치하고 Intel LibVA, Intel MediaSDK등의 의존 package들을 컴파일한 후 빌드를 하고 unittest를 하도록 하는데 14분이 넘게 걸렸다. 다음은 사용한 .travis.yml file이다.

language: cpp 

compiler:  - gcc 

dist: xenial 

env:   
  global:    
- EA_INSTALL_PREFIX=${TRAVIS_BUILD_DIR}/local    
- PATH=${EA_INSTALL_PREFIX}/bin:$PATH before_install:  
- mkdir -p ${TRAVIS_BUILD_DIR}/local  
- sudo apt-get install curl wget autoconf libtool libdrm-dev \
libboost-all-dev libgstreamer1.0-0 libasound-dev \
libgles2-mesa-dev gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc \
gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa \
gstreamer1.0-pulseaudio
- cd ${TRAVIS_BUILD_DIR}&& \
wget https://github.com/Kitware/CMake/releases/download/v3.13.2\
/cmake-3.13.2.tar.gz \
&& tar xvf cmake-3.13.2.tar.gz&&cd cmake-3.13.2&&./configure --\
prefix=${EA_INSTALL_PREFIX}&&make&&make install  
- cd ${TRAVIS_BUILD_DIR}&& \
wget https://github.com/intel/libva/archive/2.3.0.tar.gz&&\
tar xvf 2.3.0.tar.gz \
&&cd libva-2.3.0&&./autogen.sh&&./configure --\
prefix=${EA_INSTALL_PREFIX}&&make&&make install  
- cd ${TRAVIS_BUILD_DIR}&& \
wget https://github.com/Intel-Media-SDK/MediaSDK/archive/\
intel-mediasdk-18.3.1.tar.gz \
&& tar xvf intel-mediasdk-18.3.1.tar.gz&&\
cd MediaSDK-intel-mediasdk-18.3.1/&& \
cmake -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_OPENCL=OFF -DBUILD_SAMPLES=OFF .&&make&&\
make install 

script:  
- cd ${TRAVIS_BUILD_DIR} && \
cmake . && make && make install && test/ea_test

삽질2: Clear Linux docker image 사용 

시간만 오래 안 걸렸어도 기본 Ubuntu OS로 어떻게든 해보는 건데, 14분이면 시간이 너무 오래 걸린다. 이왕 시간이 오래 걸리는 거라면 타겟인 Clear Linux docker image를 사용해보자.

Clear Linux docker image를 생성하기 위한 dockerfile을 다음과 같이 작성해준 다음

FROM clearlinux

RUN clrtrust generate

RUN swupd bundle-add software-defined-cockpit-dev

.travis.yml file을 다음과 같이 선언해 준다.

language: cpp
services:
 - docker
before_install:
 - docker build -t clearlinux_ea .
 - docker run -d -v ${TRAVIS_BUILD_DIR}:/src clearlinux_ea /bin/sh -c "cd /src;cmake .;make;make install"

script:	 	 
 - docker run -d -v ${TRAVIS_BUILD_DIR}:/src clearlinux_ea /bin/sh -c "cd /src;test/ea_test"

총 소요된 시간은 17분 41초 그 중에 docker 설정하는데 걸린 시간만 16분이 넘는다. 나머지 시간에 unit test. 대부분의 시간이 docker를 빌드 하고 설정하는데 사용 되고 있었다. 

삽질3: 만들어 둔 Docker image 다운로드

빌드하는데 시간이 오래 걸린다면 이미 만들어 둔 docker image를 저장소에 넣어두고 pull해서 사용하면 좀 빠르지 않을까? Docker 빌드 vs Docker 다운로드.

이미 빌드 한 docker image를 공개 저장소인 docker hub에 넣어두고 Travis CI에서 pull하도록 변경하면 시간은 8분정도로 줄어든다.

language: cpp

services:
 - docker
	
before_install:
 - docker pull litcoder/clearlinux_ea
 - docker run -v ${TRAVIS_BUILD_DIR}:/src litcoder/clearlinux_ea /bin/sh -c "cd /src;cmake .;make;make install;"
	
script:
 - docker run -v ${TRAVIS_BUILD_DIR}:/src litcoder/clearlinux_ea /bin/sh -c "cd /src;test/ea_test"

흠.. 일단은 이걸로.

 결론

Travis CI에서 제공되는 연산 성능은 매우 떨어져서 컴파일이나 도커 빌드를 효율적으로 수행하지 못한다. 반면, 이미 만들어진 이미지의 다운로드는 상대적으로 빠르게 수행 할 수 있다. Travis CI에서 Docker를 이용한 테스트 환경을 구성하고자 한다면 미리 만들어 둔 이미지를 Docker Hub에 올려두고 CI script에서 pull 해서 사용하는 방법이 가장 고려해 볼 만한 선택이다.

[Tip] Fedora CUI booting 설정

Fedora (Version26, Workstation edition)은 /etc/inittab file로 runlevel을 변경하던 이전의 방식은 지원되지 않는다. /etc/systemd/system/default.target file을 보면 /lib/systemd/systm/graphical.target으로 link되어 있는데 이것을 multi-user.target으로 변경해 주면 CUI console로 진입할 수 있다.

$ sudo ln -sf /lib/systemd/system/multi-user.target /etc/systemd/system/default.target 

설정 후 시스템을 리붓하면 CUI로 booting되고 startx 명령으로 GUI를 불러 올 수 있다. Display Manager를 띄우지 않기 때문에 GUI에서 logout하면 CUI로 되돌아온다.

$ startx