아까 함수란 뭘까? 글을 보고 영감이 떠올라서 써본다.
먼저 C를 포함한 거의 모든 언어에선 함수를 종료할 때 그냥 함수 블록을 닫기만 하면 된다.
그렇다면 어셈블리에선 어떻게 이를 할 수 있을까?
가장 간단한 어셈블리 프로그램을 생각해보자.
다음과 같이 아무런 내용이 없는 코드를 생각해볼 수 있다.
여기서 _start는 C의 main과 같은 엔트리 포인트 역할을 하게 되고,
global _start는 이를 외부로 노출시켜 링커 등으로 하여금 이를 찾을 수 있도록 한다.
ret는 return의 준말로 진행 중이던 함수 또는 프로시저를 종료하고 이전 위치로 돌아간다.
그럼 이를 어셈블러와 링커를 이용해 실행 파일로 바꿔보자.
nasm과 ld를 이용해 실행 파일을 생성 후 실행해보면 segfault가 발생한다.
이러한 일이 발생한 이유는 위에서 작성한 ret가 C의 return과는 조금 다르기 때문이다.
어셈블리에서 ret는 단순히 이전 위치로 돌아가라는 의미로 어떠한 리턴 값도 전달하지 않는다.
때문에 코드를 실행하던 컴퓨터는 이전 위치로 돌아가라는 지시에 "모르는데 어떻게 돌아가요"를 시전하며 segfault를 뿜는 것이다.
따라서 명시적으로 반환값을 설정하여 이 프로그램이 정상적으로 종료되었음을 알려야 한다.
이와 같은 동작은 syscall이라는 커널 함수를 통해 수행할 수 있다.
아래의 코드를 보자.
위의 코드에서 rdi는 반환값을 저장할 레지스터를, rax는 사용할 syscall의 번호를 저장할 레지스터를 의미한다.
rdi에 mov 연산을 통해 0을 집어넣었으므로 반환값을 0으로 설정했음을 알 수 있다.
또, rax에는 60을 집어넣는데 이는 x86_64 Linux에서 exit syscall의 번호이다.
(여기서 한 가지 더 알 수 있는 사실이 있는데, 상기한 x86_64 Linux는 CPU 아키텍처와 운영체제의 조합으로 서로 다른 조합에서는 또 다른 어셈블리 언어가 사용된다.
x86_64 Linux와 x86_64 Windows, x86_64 Linux와 x86 Linux의 어셈블리가 다를 수 있다는 것이다.
그러나 C와 같은 언어에서는 이러한 아키텍처, 운영체제 조합을 신경쓰지 않아도 된다.
#ifdef _WIN32 등으로 떡칠된 C가 이식성이 좋다는 얘기는 바로 여기에서 기인한다.)
이제 아까와 같은 방법으로 실행 파일을 생성해 실행해보자.
정상적으로 프로그램이 종료된 후 exit code가 0임을 확인할 수 있다.
C에서는 이러한 레지스터 및 syscall 등에 대해서 사용자가 신경쓰지 않아도 되므로 C를 고수준 언어라고 부르는 것이다.
다만, 최근에는 C에서 더 추상화가 진행된 다른 언어들이 다수 존재하여 이런 언어들을 다시 메모리 관리 여부를 통해 매니지드, 언매니지드 언어로 나누기도 한다.
*번외: Hello, world
변수 선언 후 printf만 호출하면 되는 C와는 달리 어셈블리에서는 별도의 data 영역을 만들어 문자열 리터럴과 그 길이를 저장해야 한다.
문자열을 저장했으면 syscall table 을 참고하여 wrtie syscall을 호출해 문자열을 콘솔에 출력할 수 있다.
이를 실행해보면 Hello, world!가 정상적으로 출력됨을 볼 수 있다.
개추백만개주고싶네
감삼다
syscall로 호출되는 write나 exit는 C언어로 작성되어 있다는게 재밌는 부분
생각해보니 그렇네 ㄷㄷ
C는 그래도 C 코드 한 줄이 정직하게 어셈블리 몇 개 ~ 이렇게 치환이 되는 편이라 그냥 포터블 어셈블리 정도로 생각해도 좋은 듯. 진짜 고수준 언어는 템플릿 까딱 잘못 쓰면 어셈블리 수만개가 갑툭튀하는 C++같은 언어지
X86 어셈블리 명령어 하나가 ARM64 명령어 3~4개를 한 번에 수행한다고 X86을 고수준이라고 부르진 않을 거니까 ...