getopts

getopts optstring varname [args...]

쉘 에서 명령을 실행할 때 옵션을 사용하는데요. 스크립트 파일이나 함수를 실행할 때도 동일하게 옵션을 사용할 수 있습니다. 사용된 옵션은 다른 인수들과 마찬가지로 $1, $2, ... positional parameters 형태로 전달되므로 스크립트 내에서 직접 옵션을 해석해서 사용해야 됩니다. 이때 옵션 해석 작업을 쉽게 도와주는 명령이 getopts 입니다.

옵션에는 short 옵션과 long 옵션이 있는데 getopts 명령은 short 옵션을 처리합니다.

short 옵션의 특징

short 옵션은 다음과 같이 여러 가지 방법으로 사용할 수 있습니다. 그러므로 getopts 명령을 이용하지 않고 직접 옵션을 해석해 처리한다면 옵션 처리에만 스크립트가 복잡해질 수 있습니다.

$ command -a -b -c

# 옵션을 붙여서 사용할 수 있으며 순서가 바뀌어도 된다.
$ command -abc
$ command -b -ca

# 옵션인수를 가질 수 있다.
$ command -a xxx -b -c yyy

# 옵션인수를 옵션에 붙여 쓸 수가 있다.
# 그러므로 다음은 위의 예와 동일하게 해석됩니다.
$ command -axxx -bcyyy

# 옵션 구분자 '--' 가 올경우 우측에 있는 값은 옵션으로 해석하면 안된다.
$ command -a -b -- -c

long 옵션의 경우

--posix, --warning level 와 같은 형태로 사용되는 long 옵션은 short 옵션과는 달리 붙여 쓸 수가 없기 때문에 사용방법이 간단하여 직접 해석해서 처리하는 것이 어렵지 않습니다. 그러므로 먼저 short 옵션 처리 방법에 대해 알아보고 뒷부분에서 long 옵션에 대해 알아보겠습니다.

Option string 과 $OPTIND

명령에서 -a, -b, -c 세 개의 옵션을 사용한다면 getopts 명령에 설정하는 optstring 값은 abc 가 됩니다. args 에는 명령 실행시 사용된 인수 값들이 오는데 생략할 경우 "$@" 가 사용됩니다.

다음 예제를 통해서 getopts 명령에서 사용되는 optstring, varname 과 $OPTIND 에 대해 알아보겠습니다. shell 이 처음 실행되면 $OPTIND 값은 1 을 가리키고 getopts 명령이 실행될 때마다 다음 옵션의 index 값을 가리키게 됩니다.

# 다음 set 명령은 실제 "command -a -bc hello world" 명령을 실행했을 때와 같이
# positional parameters 를 설정합니다.
$ set -- -a -bc hello world
$ echo "$@"
-a -bc hello world

# 처음 $OPTIND 값은 1 로 -a 를 가리킵니다.
$ echo $OPTIND
1

$ getopts abc opt "$@"            # getopts 명령을 실행할 때마다 opt 변수값과
$ echo $opt, $OPTIND              # OPTIND 값이 변경되는 것을 볼 수 있습니다.
a, 2                              # 다음 옵션 "b" 의 index 값은 2 가 됩니다.

$ getopts abc opt "$@"            # 다음 옵션 "c" 는 "b" 와 붙여 쓰기를 하여 같은
$ echo $opt, $OPTIND              # OPTIND 값을 가집니다.
b, 2                              # ( sh 의 경우에는 b, 3 가 출력됩니다. )

$ getopts abc opt                 # args 부분을 생략하면 default 값은 "$@" 입니다.
$ echo $opt, $OPTIND
c, 3

# 옵션 스트링을 처리하고 난후 다음과 같이 shift 를 하면 나머지 명령 인수만 남게 됩니다.

$ echo "$@"
-a -bc hello world

$ shift $(( OPTIND - 1 ))
$ echo "$@"
hello world

옵션을 붙여쓰기 할 경우 shbash$OPTIND 값을 처리하는 방식이 약간 다릅니다.
하지만 최종적으로 shift $(( OPTIND - 1 )) 한 값은 둘 다 같게 됩니다.

set -- -abc hello world

echo $OPTIND

getopts abc opt 
echo $opt, $OPTIND 

getopts abc opt 
echo $opt, $OPTIND

getopts abc opt  
echo $opt, $OPTIND

shift $(( OPTIND - 1 ))
echo "$@"
---------------------------

#!/bin/bash 의 경우
1           # index 1 은 -abc 를 가리킴
a, 1        # index 1 은 -abc 를 가리킴
b, 1        # index 1 은 -abc 를 가리킴
c, 2        # index 2 는 hello 를 가리킴
hello world

#!/bin/sh 의 경우
1           # index 1 은 -abc 를 가리킴
a, 2        # index 2 는 hello 를 가리킴
b, 2        # index 2 는 hello 를 가리킴
c, 2        # index 2 는 hello 를 가리킴
hello world

Option argument

옵션은 옵션인수를 가질 수 있는데요. 이때는 옵션 스트링에서 해당 옵션 문자 뒤에 : 을 붙입니다.
그러면 getopts 명령은 옵션인수 값를 $OPTARG 변수에 설정해 줍니다.

$ OPTIND=1
$ set -- -a xyz -b -c hello world

# 옵션 a 가 옵션인수를 가지므로 옵션 스트링으로 a: 를 설정하였습니다.
$ getopts a:bc opt
$ echo $opt, $OPTARG, $OPTIND        
a, xyz, 3                        # 옵션인수가 포함되므로 OPTIND 값이 3 이 되었음.

$ getopts a:bc opt
$ echo $opt, $OPTARG, $OPTIND
b, , 4

$ getopts a:bc opt
$ echo $opt, $OPTARG, $OPTIND
c, , 5

loop 문을 이용해 처리

명령문에서 사용된 모든 옵션을 처리하기 위해서 다음과 같이 while 과 case 문을 이용합니다.

#!/bin/bash

while getopts "a:bc" opt; do
  case $opt in
    a)
      echo >&2 "-a was triggered!, OPTARG: $OPTARG"
      ;;
    b)
      echo >&2 "-b was triggered!"
      ;;
    c)
      echo >&2 "-c was triggered!"
      ;;
  esac
done

shift $(( OPTIND - 1 ))
echo "$@"
..................................

$ ./test.sh -a xyz -bc hello world
-a was triggered!, OPTARG: xyz
-b was triggered!
-c was triggered!
hello world

Error reporting

위 예제에서 옵션 스트링에 없는 문자, 예를 들면 -d 를 사용하게 되면 오류 메시지가 출력되는 것을 볼 수 있는데요. getopts 명령은 error reporting 과 관련해서 다음과 같은 두 개의 모드를 제공합니다.

Verbose mode
invalid 옵션 사용 opt 값을 ? 문자로 설정하고 OPTARG 값은 unset. 오류 메시지를 출력.
옵션인수 값을 제공하지 않음 opt 값을 ? 문자로 설정하고 OPTARG 값은 unset. 오류 메시지를 출력.
Silent mode
invalid 옵션 사용 opt 값을 ? 문자로 설정하고 OPTARG 값은 해당 옵션 문자로 설정
옵션인수 값을 제공하지 않음 opt 값을 : 문자로 설정하고 OPTARG 값은 해당 옵션 문자로 설정

default 는 verbose mode 인데 기본적으로 옵션과 관련된 오류메시지가 표시되므로 스크립트를 배포할 때는 잘 사용하지 않고 대신 silent mode 를 이용합니다. silent mode 를 설정하기 위해서는 옵션 스트링의 맨 앞부분에 : 문자를 추가해 주면 됩니다.

#!/bin/bash

# silent mode 를 설정하기 위해 옵션 스트링의 맨 앞부분에 ':' 문자를 추가
while getopts ":a:" opt; do
  case $opt in
    a)
      echo >&2 "MSG: -a was triggered, argument: $OPTARG"
      ;;
    \?)   # \? 는 escape 했으므로 glob 문자가 아님.
      echo >&2 "ERR: Invalid option: -$OPTARG"
      exit 1
      ;;
    :)
      echo >&2 "ERR: Option -$OPTARG requires an argument."
      exit 1
      ;;
  esac
done
------------------------------------------------------

$ ./test.sh -a 
ERR: Option -a requires an argument.

$ ./test.sh -a xyz
MSG: -a was triggered, argument: xyz

$ ./test.sh -d
ERR: Invalid option: -d

주의할 점

OPTIND, OPTARG 변수는 local 변수가 아니므로 필요할 경우 함수 내에서 local 로 설정해 사용해야 합니다. getopts 명령은 옵션인수 사용과 관련해서 다음과 같이 주의할 점이 있습니다.

# 옵션 스트링이 'a:bc' 이면 -a 는 옵션인수를 갖는데요. 옵션인수는 어떤 문자도 올 수 있기 때문에
# 다음과 같이 -a 에 옵션인수가 설정되지 않으면 -b 가 -a 의 옵션 인수가 됩니다.
$ command -a -b -c

# 파일명이나 기타 스트링은 마지막에 와야하는데 그렇지 않을 경우 이후 옵션은 인식되지 않습니다.
# 다음의 경우 옵션 스트링이 'abc' 라면 -b -c 옵션은 인식되지 않습니다.
$ command -a foo.c -b -c

예제 )

#!/bin/bash

usage() {
    err_msg "Usage: $(basename "$0") -s <45|90> -p <string>"
    exit 1
}

err_msg() { echo "$@" ;} >&2
err_msg_s() { err_msg "-s option argument 45 or 90 required" ;}
err_msg_p() { err_msg "-p option argument required" ;}

while getopts ":s:p:" opt; do
    case $opt in
        s)
            s=$OPTARG
            [ "$s" = 45 ] || [ "$s" = 90 ] || { err_msg_s; usage ;}
            ;;
        p)
            p=$OPTARG    # -p -s 45 또는 -p -s45 일경우
            [[ $p =~ ^-s ]] && { err_msg_p; usage ;}
            ;;
        :)
            case $OPTARG in
                s) err_msg_s ;;
                p) err_msg_p ;;
            esac
            usage
            ;;
        \?)
            err_msg "Invalid option: -$OPTARG"
            usage
            ;;
    esac
done

if [ -z "$s" ] || [ -z "$p" ]; then
    usage
fi

echo "s = $s"
echo "p = $p"

Long 옵션의 처리

$ ./test.sh -a aaa --posix --long 123 -b --warning=2 -- hello world

short 옵션 처리에 대해 알아보았는데요. 만약에 위와 같은 명령문을 getopts 으로 처리한다면 옵션 스트링으로 :a:b- 를 사용하고 case 문에서는 -) 를 사용해야 -- 로 시작하는 long 옵션을 받을 수 있습니다. 그런데 여기서 문제는 short 옵션은 하나의 문자를 옵션으로 보기 때문에 이후에 p, o, s, i, x 가 모두 붙여쓰기한 옵션명으로 인식을 하게 됩니다. 또 한 가지 문제점은 위의 --long 옵션과 같이 123 옵션인수를 사용하게 되면 그 이후의 옵션 그러니까 -b 는 getopts 에 의해 인식이 되지 않습니다.

따라서 getopts 명령으로 short, long 옵션을 동시에 처리하는 것은 어려우므로 먼저 long 옵션을 처리하고 난후 나머지 short 옵션만 정리하여 getopts 에 넘겨주면 이전과 동일하게 short 옵션을 처리할 수 있습니다.

# 처리한 long 옵션은 삭제하고 short 옵션만 getopts 명령에 전달.
-a aaa -b -- hello world

long 옵션을 $@ 에서 삭제하는 방법은 while 문을 이용해 인수들을 하나씩 처리하면서 long 옵션이 아닐 경우 특정 변수에 계속해서 append 하는 것입니다. 마지막으로 long 옵션이 제거된 변수를 이용해 set 명령으로 다시 $@ 값을 설정합니다.

# shift 명령을 이용해 항상 '$1' 에는 다음 인수가 설정되게 합니다.
while true; do
    case $1 in
        --optionA)
        ...
        shift; continue   # long 옵션일 경우 continue 명령을 사용함으로써
        ;;                # 변수 'A' 에 값이 저장되지 않게 합니다.
        --optionB)
        ...
        ...
    esac

    # long 옵션이 아닐 경우 변수 'A' 에 append 하는 과정입니다.
    # 이때 인수들의 구분자로 non-printing 문자인 '\a' 를 사용합니다.
    # (공백을 사용하게 되면 인수값에 공백이 포함될 경우 인수값도 분리가 됩니다)
    A=$A$([ -n "$A" ] && echo -e "\a")$1
    shift
done

# 마지막으로 set -f 로 globbing 방지 처리를 하고 IFS 값을 '\a' 로 설정하여 
# set -- $A 명령으로 '$@' 값을 다시 설정합니다.
IFS=$(echo -e "\a"); set -f; set -- $A; set +f; IFS=$(echo -e " \n\t")

다음은 전체 코드 내용입니다.

sh 에서 사용하려면 본문의 echo -eecho 로 변경해 주면 됩니다.

#!/bin/bash

usage() {
    err_msg "Usage: $0 -a <string> -b --long <string> --posix --warning[=level]"
    exit 1
}

err_msg() { echo "$@" ;} >&2
err_msg_a() { err_msg "-a option argument required" ;}
err_msg_l() { err_msg "--long option argument required" ;}

########################## long 옵션 처리 부분 #############################
A=""
while true; do
    [ $# -eq 0 ] && break
    case $1 in
        --long) 
            shift    # 옵션인수를 위한 shift
            # --long 은 옵션인수를 갖는데 옵션인수가 오지 않고 다른 옵션명(-*) 이 오거나
            # 명령의 끝에 위치하여 옵션인수가 설정되지 않았을 경우 ("")
            case $1 in (-*|"") err_msg_l; usage; esac
            err_msg "--long was triggered!, OPTARG: $1"
            shift; continue
            ;;
        --warning*)
            # '=' 로 분리된 level 값을 처리하는 과정입니다.
            case $1 in (*=*) level=${1#*=}; esac
            err_msg "--warning was triggered!, level: $level"
            shift; continue
            ;;
        --posix) 
            err_msg "--posix was triggered!"
            shift; continue
            ;;
        --)
            # '--' 는 옵션의 끝을 나타내므로 나머지 값 '$*' 을 A 에 append 하고 break 합니다.
            # 이때 IFS 값을 '\a' 로 변경해야 $* 내의 인수 구분자가 '\a' 로 됩니다.
            IFS=$(echo -e "\a")
            A=$A$([ -n "$A" ] && echo -e "\a")$*
            break
            ;;
        --*) 
            err_msg "Invalid option: $1"
            usage;
            ;;
    esac

    A=$A$([ -n "$A" ] && echo -e "\a")$1
    shift
done

# 이후부터는 '$@' 값에 short 옵션만 남게 됩니다.
# -a aaa -b -- hello world 
IFS=$(echo -e "\a"); set -f; set -- $A; set +f; IFS=$(echo -e " \n\t")

########################## short 옵션 처리 부분 #############################

# 이전과 동일하게 short 옵션을 처리하면 됩니다.
while getopts ":a:b" opt; do
    case $opt in
        a)
            case $OPTARG in (-*) err_msg_a; usage; esac
            err_msg "-a was triggered!, OPTARG: $OPTARG"
            ;;
        b)
            err_msg "-b was triggered!"
            ;;
        :)
            case $OPTARG in
                a) err_msg_a ;;
            esac
            usage
            ;;
        \?)
            err_msg "Invalid option: -$OPTARG"
            usage
            ;;
    esac
done

shift $(( OPTIND - 1 ))
echo ------------------------------------
echo "$@"

########################## 실행 결과  #############################

$ ./test.sh -a 'aaa bbb' --posix --long 123 -b --warning=2 -- hello world 
--posix was triggered!
--long was triggered!, OPTARG: 123
--warning was triggered!, level: 2
-a was triggered!, OPTARG: aaa bbb
-b was triggered!
------------------------------------
hello world

getopt 외부 명령

getopt 은 이름이 getopts builtin 명령과 비슷한데 /usr/bin/getopt 에 위치한 외부 명령입니다. 이 명령은 기본적으로 short, long 옵션을 모두 지원합니다. 옵션 인수를 가질 경우 : 문자를 사용하는 것은 getopts builtin 명령과 동일합니다.

# short 옵션 지정은 -o 옵션으로 합니다.
# ':' 에 따라서 옵션 -a 는 옵션 인수를 갖습니다. 
getopt -o a:bc

# long 옵션 지정은 -l 옵션으로 하고 옵션명은 ',' 로 구분합니다.
# ':' 에 따라서 옵션 --path 와 --name 은 옵션 인수를 갖습니다.
# 명령 라인에서 옵션 인수 사용은 "--name foo" 또는 "--name=foo" 두 가지 모두 가능합니다.
getopt -l help,path:,name: 

# 명령 마지막에는 -- 와 함께 "$@" 를 붙입니다.
getopt -o a:bc -l help,path:,name: -- "$@"

설정하지 않은 옵션이 사용되거나 옵션 인수가 빠질 경우 오류메시지를 출력해줍니다.

#!/bin/bash

options=$( getopt -o a:bc -l help,path:,name: -- "$@" )
echo "$options"
-----------------------------------------------------

$ ./test.sh -x
getopt: invalid option -- 'x'

$ ./test.sh --xxx
getopt: unrecognized option '--xxx'

$ ./test.sh -a
getopt: option requires an argument -- 'a'

$ ./test.sh --name
getopt: option '--name' requires an argument

getopt 명령의 특징은 사용자가 입력한 옵션들을 case 문에서 사용하기 좋게 정렬해준다는 것입니다.

#!/bin/bash

options=$( getopt -o a:bc -l help,path:,name: -- "$@" )
echo "$options"
-----------------------------------------------------

# 1. -a123 옵션이 -a '123' 로 분리.
# 2. -bc 옵션이 -b -c 로 분리.
# 3. 옵션에 해당하지 않는 hello.c 는 '--' 뒤로 이동
$ ./test.sh -a123 -bc hello.c
-a '123' -b -c -- 'hello.c'

# --path=/usr/bin 옵션이 --path '/usr/bin' 로 분리
# '--' 는 항상 끝부분에 붙는다.
$ ./test.sh --name foo --path=/usr/bin
--name 'foo' --path '/usr/bin' --

# '--' ( end of options ) 처리도 해줍니다.
$ ./test.sh -a123 -bc hello.c -- -x --yyy
 -a '123' -b -c -- 'hello.c' '-x' '--yyy'

getopts builtin 명령의 경우 옵션들 중간에 파일명이 온다거나 하면 이후의 옵션은 옵션으로 인식이 되지 않는데 getopt 명령의 경우는 올바르게 구분하여 정렬해 줍니다.

[ getopts builtin 명령의 경우 ]

#!/bin/bash

while getopts "a:bc" opt; do
  case $opt in
    a)
      echo >&2 "-a was triggered!, OPTARG: $OPTARG"
      ;;
    b)
      echo >&2 "-b was triggered!"
      ;;
    c)
      echo >&2 "-c was triggered!"
      ;;
  esac
done

shift $(( OPTIND - 1 ))
echo ------------------
echo "$@"
...............................................................

./test.sh -a 123 -bc hello.c
-a was triggered!, OPTARG: 123
-b was triggered!
-c was triggered!
------------------
hello.c

# 옵션들 중간에 파일명이 위치하여 이후 -bc 는 옵션으로 인식되지 않는다.
$ ./test.sh -a 123 hello.c -bc
-a was triggered!, OPTARG: 123
------------------
hello.c -bc

getopt 외부 명령의 경우는 옵션에 해당하지 않는 hello.c 를 -- 뒤로 정렬해 줍니다.

#!/bin/bash

options=$(getopt -o a:bc -- "$@")
echo $options
.................................

$ ./test.sh -a 123 hello.c -bc
-a '123' -b -c -- 'hello.c'

예제 )

#!/bin/bash

if ! options=$(getopt -o hp:n: -l help,path:,name:,aaa -- "$@")
then
    echo "ERROR: print usage"
    exit 1
fi

eval set -- "$options"

while true; do
    case "$1" in
        -h|--help) 
            echo >&2 "$1 was triggered!"
            shift ;;
        -p|--path)    
            echo >&2 "$1 was triggered!, OPTARG: $2"
            shift 2 ;;   # 옵션 인수를 가지므로 shift 2 를 합니다.
        -n|--name)
            echo >&2 "$1 was triggered!, OPTARG: $2"
            shift 2 ;;
        --aaa)     
            echo >&2 "$1 was triggered!"
            shift ;;
        --)           
            shift 
            break
    esac
done

echo --------------------
echo "$@"
-----------------------------------------------------------------

$ ./test.sh -h hello.c -p /usr/bin --name='foo bar' --aaa -- --bbb
-h was triggered!
-p was triggered!, OPTARG: /usr/bin
--name was triggered!, OPTARG: foo bar
--aaa was triggered!
--------------------
hello.c --bbb

Quiz

명령에 인수를 전달할 때 command line 뿐만 아니라 파이프나 redirection 을 이용해 전달하려면 어떻게 할까요?

파이프나 redirection 을 이용하면 명령의 stdin 에 연결이 되므로 read 명령을 이용해 값을 읽어들이면 됩니다. 그런데 이때 값을 읽어들이기 위해 단순히 read 명령을 사용하면 전달되는 값이 없을 경우 입력 대기 상태로 블록이 됩니다. 따라서 read 하기 전에 먼저 -t ( timeout ) 옵션 값을 0 으로 설정하여 읽어들일 라인이 있는지 테스트해야 합니다.

#!/bin/bash

test $# != 0 && { 
    echo "from command line"
    for (( i = 1; i <= $#; i++ )) do echo "\$$i : ${!i}"; done
}

set --

read -t 0 && { args=""      # read 하기 전에 먼저 읽어들일 라인이 있는지 테스트 
    while read -r line; do args+=" $line"; done
    set -f; set -- $args; set +f

    # 만약에 command line 옵션과 합치려면 위에서 set -- 을 제거하고 다음과 같이 하면 됩니다.
    # set -f; set -- "$@" $args; set +f
}

test $# != 0 && {
    echo "from STDIN"
    for (( i = 1; i <= $#; i++ )) do echo "\$$i : ${!i}"; done
}

----------------------------------------------------------------

$ ./test.sh 11 22 33                  $ echo 11 22 33 | ./test.sh       
from command line                     from STDIN
$1 : 11                               $1 : 11
$2 : 22                               $2 : 22
$3 : 33                               $3 : 33

                                      $ ./test.sh <<< "11 22 33" 
                                      from STDIN
                                      $1 : 11
                                      $2 : 22
                                      $3 : 33

                                      $ ./test.sh <<\EOF
                                      > 11 22 33
                                      > EOF
                                      from STDIN
                                      $1 : 11
                                      $2 : 22
                                      $3 : 33

                       $ ./test.sh 11 22 33 <<\EOF
                       foo
                       bar
                       zoo
                       EOF
                       from command line
                       $1 : 11
                       $2 : 22
                       $3 : 33
                       from STDIN
                       $1 : foo
                       $2 : bar
                       $3 : zoo

read -t 0 옵션을 사용할 수 없는 sh 에서는 다음과 같이 변경하면 됩니다.

read -t 0 && { args=""
    while read -r line; do args+=" $line"; done
    set -f; set -- $args; set +f
}
-----------------------------------------------

args=$( dd iflag=nonblock 2> /dev/null ) || true
set -f; set -- $args; set +f

2 .

objdump 명령을 이용해 disassemble 을 해보려면 먼저 컴파일 과정을 거쳐서 object 파일이나 실행파일을 만들어야 되는데요. 스크립트를 이용해 C 소스 파일을 직접 objdump 합니다. 추가로 gcc-arm-linux-gnueabi, gcc-mips-linux-gnu, gcc-riscv64-linux-gnu ... 패키지도 설치해서 여러 아키텍쳐 별로도 조회해볼 수 있게 합니다.

1 . 디폴트 아키텍쳐는 x86 이고 실행파일을 만들어 덤프 합니다.

$ objdump.sh hello.c

2 . 링크는 하지 않고 컴파일만 해서 오브젝트 파일을 덤프 합니다.

$ objdump.sh -c hello.c

3 . func1 함수 부분만 출력합니다.

$ objdump.sh -f func1 hello.c

4 . arm cpu 용으로 컴파일해서 덤프 합니다.

$ objdump.sh -a arm -f func1 hello.c

5 . 기타 gcc 옵션을 주려면 -- 와 함께 마지막에 추가하면 됩니다.

$ objdump.sh -a arm -f func1 hello.c -- -O2

#!/bin/sh

trap 'exit' HUP INT QUIT TERM
trap 'rm -f $tmpfile' EXIT

err() { echo "$@" ;} >&2

usage() {

    cat <<END 

Usage: $(basename "$0") [-a arch] [-f func] [-c] [-h] file
-------------------------------------------------------------------
arch: arm armhf aarch64 mips mips64 ppc ppc64 s390x sparc64 riscv64
-------------------------------------------------------------------
END

    exit 1

} >&2

test $# = 0 && usage

options=$(getopt -o a:f:ch -- "$@") || usage
eval set -- "$options"

while true; do
  case $1 in
    -a)
        arch=$2; shift 2 ;;     # -a 는 옵션 인수를 가지므로 shift 2 를 합니다.
    -f)
        func=$2; shift 2 ;;
    -c)
        comp=$1; shift ;;
    -h)
        usage ;;
    --) 
        shift; break
  esac
done

if test $# -ge 1; then
    file=$1
    shift
else
    usage
fi

test -z "$arch" && arch=x86

case $arch in
    x86)
        gcc=gcc
        objdump=objdump
        ;;

    aarch64)
        gcc=aarch64-linux-gnu-gcc
        objdump=aarch64-linux-gnu-objdump
        ;;

    arm)
        gcc=arm-linux-gnueabi-gcc
        objdump=arm-linux-gnueabi-objdump
        ;;

    armhf)
        gcc=arm-linux-gnueabihf-gcc
        objdump=arm-linux-gnueabihf-objdump
        ;;

    mips64)
        gcc=mips64-linux-gnuabi64-gcc
        objdump=mips64-linux-gnuabi64-objdump
        ;;

    mips)
        gcc=mips-linux-gnu-gcc
        objdump=mips-linux-gnu-objdump
        ;;

    sparc64)
        gcc=sparc64-linux-gnu-gcc
        objdump=sparc64-linux-gnu-objdump
        ;;

    ppc64)
        gcc=powerpc64-linux-gnu-gcc
        objdump=powerpc64-linux-gnu-objdump
        ;;

    ppc)
        gcc=powerpc-linux-gnu-gcc
        objdump=powerpc-linux-gnu-objdump
        ;;

    s390x)
        gcc=s390x-linux-gnu-gcc
        objdump=s390x-linux-gnu-objdump
        ;;

    riscv64)
        gcc=riscv64-linux-gnu-gcc
        objdump=riscv64-linux-gnu-objdump
        ;;

    *)
        err
        err "ERROR: unknown architecture"
        usage

esac

tmpfile=`mktemp -p /dev/shm`

$gcc -g $comp -o $tmpfile "$file" "$@" && 
if test -n "$func"; then
    $objdump -wCS $tmpfile |
    sed -En '/^[[:xdigit:]]+ <\.?'"$func"'>:$/,/^[[:xdigit:]]+ <[^>]+>:$/p'
else
    $objdump -wCS $tmpfile
fi

CISC 인 x86 의 경우 인스트럭션 사이즈가 가변인데 반해 RISC 는 일정한 것을 볼 수 있습니다.

$ objdump.sh hello.c | awk -F '\t' '$1 && $2 ~ /[^ ]/ { 
    a[length(gensub(/ /,"","g",$2)),i++] = $2 }
    END { PROCINFO["sorted_in"] = "@ind_num_desc"; for (i in a) print a[i] }
'
66 66 2e 0f 1f 84 00 00 00 00 00    # x86 은 1 ~ 12 bytes 까지 가변
66 2e 0f 1f 84 00 00 00 00 00 
48 83 3d e2 2e 00 00 00 
4c 8d 3d 3b 2c 00 00 
. . .
---------------------------------------------------------------

$ objdump.sh -a arm hello.c | awk -F '\t' '$1 && $2 ~ /[^ ]/ { 
    a[length(gensub(/ /,"","g",$2)),i++] = $2 }
    END { PROCINFO["sorted_in"] = "@ind_num_desc"; for (i in a) print a[i] }
'
08bd87f0      # RISC 는 일정
e1b06146 
ebffff9d 
e1a09002 
e1a08001 
. . .

qemu 의 유저 모드 에뮬레이션을 이용하면 실행과 디버깅도 가능합니다. 유저 모드 에뮬레이션은 하드웨어 디바이스는 에뮬레이션 하지 않고 cpu instruction 과 system call 을 에뮬레이션 합니다.

$ sudo apt install qemu-user gdb-multiarch  

$ arm-linux-gnueabi-gcc -g -o hello.arm hello.c

$ file hello.arm
hello.arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked,
interpreter /lib/ld-linux.so.3, BuildID[sha1]=f6f1a7f2795138c3cefdaa4705c2d32de1bdbc98,
for GNU/Linux 3.2.0, with debug_info, not stripped

# 실행파일을 -static 으로 빌드하면 -L ... 옵션은 필요없습니다.
$ qemu-arm -L /usr/arm-linux-gnueabi hello.arm  
hello world

# strace
$ qemu-arm -L /usr/arm-linux-gnueabi -strace hello.arm
24255 brk(NULL) = 0x00022000
24255 uname(0xfffed5f8) = 0
. . .
------------------------------------------------------

# terminal 1
$ qemu-arm -L /usr/arm-linux-gnueabi -g 1234 hello.arm  

# terminal 2
$ gdb-multiarch -q hello.arm
Reading symbols from hello.arm...
(gdb) set sysroot /usr/arm-linux-gnueabi
(gdb) target remote :1234
Remote debugging using :1234
Reading symbols from /usr/arm-linux-gnueabi/lib/ld-linux.so.3...
(No debugging symbols found in /usr/arm-linux-gnueabi/lib/ld-linux.so.3)
0xff7bca40 in ?? () from /usr/arm-linux-gnueabi/lib/ld-linux.so.3
(gdb) b main
Breakpoint 1 at 0x104ac: file hello.c, line 15.
(gdb) c
Continuing.

Breakpoint 1, main () at hello.c:15
15          int res = gvar1 + gvar2;
(gdb) disas main
Dump of assembler code for function main:
   0x000104a0 <+0>:     push    {r11, lr}
   0x000104a4 <+4>:     add     r11, sp, #4
   0x000104a8 <+8>:     sub     sp, sp, #8
=> 0x000104ac <+12>:    ldr     r3, [pc, #52]   ; 0x104e8 <main+72>
   0x000104b0 <+16>:    ldr     r2, [r3]
   0x000104b4 <+20>:    ldr     r3, [pc, #48]   ; 0x104ec <main+76>
   0x000104b8 <+24>:    ldr     r3, [r3]
   0x000104bc <+28>:    add     r3, r2, r3
   . . .

한가지 tip 으로 binfmt-support, qemu-user-static 패키지를 설치하면 static 으로 빌드된 실행파일의 경우 qemu-arm 명령 없이도 프롬프트 상에서 직접 실행할 수 있습니다. dynamic link 로 빌드된 실행파일도 가능한데 다음과 같이 설정해 주면 됩니다.

mkdir /etc/qemu-binfmt
ln -s /usr/arm-linux-gnueabihf /etc/qemu-binfmt/arm
ln -s /usr/aarch64-linux-gnu /etc/qemu-binfmt/aarch64
ln -s /usr/mips-linux-gnu /etc/qemu-binfmt/mips
ln -s /usr/mips64-linux-gnuabi64 /etc/qemu-binfmt/mips64
ln -s /usr/powerpc-linux-gnu /etc/qemu-binfmt/ppc
ln -s /usr/powerpc64-linux-gnu /etc/qemu-binfmt/ppc64
ln -s /usr/sparc64-linux-gnu /etc/qemu-binfmt/sparc64
ln -s /usr/riscv64-linux-gnu /etc/qemu-binfmt/riscv64
-------------------------------------------------------

$ ls -l /proc/sys/fs/binfmt_misc

# kernel 이 ARM ELF magic 을 인식해서 해당 interpreter 로 실행합니다.
$ cat /proc/sys/fs/binfmt_misc/qemu-arm
enabled
interpreter /usr/bin/qemu-arm-static
flags: OCF
offset 0
magic 7f454c4601010100000000000000000002002800
mask ffffffffffffff00fffffffffffffffffeffffff

Docker 활용하기

도커를 이용하면 x86 이외의 arm32v5, arm32v7, arm64v8, ppc64le, s390x 아키텍쳐 리눅스 시스템을 유저모드에서 사용할 수 있습니다. base 시스템 디렉토리 구조가 생성되고 필요한 패키지를 설치해 사용할 수 있습니다. 터미널에서 ps 명령을 실행해 보면 /usr/bin/qemu-aarch64-static 에의해 bash 가 실행된 것을 볼 수 있습니다.

관련주소: https://hub.docker.com/r/arm64v8/debian/

$ docker pull arm64v8/debian
. . . .

$ docker run -ti arm64v8/debian     

root@f6a0542a2fd9:/# ls
bin   dev  home  media  opt   root  sbin  sys  usr
boot  etc  lib   mnt    proc  run   srv   tmp  var

root@f6a0542a2fd9:/# apt update
Get:1 http://security-cdn.debian.org/debian-security stretch/updates InRelease [94.3 kB]   
Ign:2 http://cdn-fastly.deb.debian.org/debian stretch InRelease                            
. . . .

root@f6a0542a2fd9:/# apt install procps file            
Reading package lists... Done
Building dependency tree       
. . . .

root@f6a0542a2fd9:/# ps ax
  PID TTY      STAT   TIME COMMAND
    1 pts/0    Ssl    0:00 /usr/bin/qemu-aarch64-static /bin/bash
  211 ?        Rl+    0:00 /bin/ps ax

root@f6a0542a2fd9:/# file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked,
interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0,
BuildID[sha1]=a242161239cb1f4ea9b8a7455013295e5473e3ec, stripped