[C Lab] How-To inline assembly & test
Project

[C Lab] How-To inline assembly & test

반응형

Linux Kernel에 존재하는 시스템 콜을 직접 구현해보면서 inline assembly 문법에 대해 궁금해져 쓰는 글이다. 레퍼런스는 이 글의 제일 아래에 명시해놓았다.

 

How-To inline assembly

Why we use the inline assembly?

이유는 몇 가지가 있다.
1) 시스템 성능 최적화에 제격이라
2) 어셈블리어로 작업할게 필요한데 그때마다 서브루틴의 프롤로그와 에필로그의 작성이 번거로울 때
이외에도 있을 텐데 내가 아는 선에서는 그렇다.

Grammar

__asm__ __volatile__ (asms : output : input : clobber );
  • __asm__ : asm으로도 쓸 수 있지만, ansi 옵션일 때는 asm이 정의되지 않기 때문에 되도록이면 __asm__으로 쓰는 것이 좋다.
  • __volatile__ : 컴파일러에게 optimization을 수행하지 말라는 표시이다. 이게 없다면 loop가 생기는 상황에서 컴파일러가 임의로 로직을 최적화할 수도 있다. 별 효과가 없을지 몰라도 없으면 가끔 이상한 값이 나올 때도 있다. 필자는 동기화 문제가 발생하면 이를 막기 위해 이 옵션을 사용하는 게 좋을 것이라 생각한다.
  • asms : 큰따옴표로 둘러싸인 어셈블리 명령어들. %n의 형태로 내부에서 input, output의 인자들을 사용할 수 있다.
  • output : 쉼표로 구분된 variable들의 리스트. 각각 inline assembly에서 쓰이는 output 인자들이다.
  • input : output과 같은 형태.
  • clobber : 쉼표로 구분되는 레지스터 이름들의 리스트. input, output에 나오지는 않았지만 값의 변경이 일어나는 레지스터들을 나타낸다.

Example)

int test_and_set_bit(int nr, volatile unsigned * addr)
{
	int oldbit;

	__asm__ __volatile__(
		"lock; btsl %2,%1\n\tsbbl %0,%0"
		:"=r" (oldbit),"=m" (*addr)
		:"r" (nr));
		return oldbit;
}

위의 결과는 다음과 같다.

lock; btsl nr, *addr
sbbl oldbit

output에서 보이는 =는 inline assembly의 결과로 값이 변경될 수 있음을 나타낸다. output 변수들은 이 modifier(=)가 반드시 지정되어 있어야 한다.

간혹 보이는 =& 형태가 있는데 여기서 &는 Early Clobber이다. Early Clobber은 gcc가 input 변수들이 다 쓰이고 난 이후에 output 변수들이 사용된다고 가정하고 input, output 변수들을 같은 operand에 할당하기도 한다. 하지만 이는 종종 잘못된 결과를 초래하기도 한다. 예를 들어 a + b + c = sum을 구하려고 하는데 sum과 a 변수의 값을 담는 operand가 edx로 같게 설정될 수도 있다는 뜻이다. 그래서 미리 gcc에게 output 변수들이 도중에 사용되어 값이 변경될 수도 있다는 점을 알려주기 위해 & modifier를 사용한다.

Clobber는 input, output에서는 사용되지는 않지만 inline assembly 내에서 사용되는 변수들을 기술한다. 이는 위에서 설명한 Early Clobber과 마찬가지로 연산 도중에 사용되는 input, output 변수들과 같은 operand를 사용해서 값이 변형되는 일을 막기 위함이다. 적어줄 때에는 겹치는 레지스터를 직접 명시해주어야 한다.

 

Test

덧뺄셈 구현하기 (Environment: WSL2 Ubuntu20.04 Kernel v5.10.16.3)

inline_assembly.c

#include <stdio.h>

int main()
{
        int a = 10, b = 5, result;

        __asm__ __volatile__(
                "movl   %1, %0          \n\t"
                "addl   %2, %0          \n\t"
                : "=g" (result)
                : "g" (a), "g" (b));

        printf("a + b = %d\n", result);

        __asm__ __volatile__(
                "movl   %1, %0          \n\t"
                "subl   %2, %0          \n\t"
                : "=g" (result)
                : "g" (a), "g" (b));

        printf("a - b = %d\n", result);

        return 0;
}

a = 10, b = 5 초기화 변수로 덧뺄셈 결과를 보여주는 간단한 코드다. 결과는 정상적으로 잘 나오며 "gcc -S inline_assembly.c"로 나오는 결과를 보자.

        .file   "inline_assembly.c"
        .text
        .section        .rodata
.LC0:
        .string "a + b = %d\n"
.LC1:
        .string "a - b = %d\n"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        endbr64
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movl    $10, -12(%rbp)
        movl    $5, -8(%rbp)
#APP
# 8 "inline_assembly.c" 1
        movl    -12(%rbp), %eax
        addl    -8(%rbp), %eax

# 0 "" 2
#NO_APP
        movl    %eax, -4(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, %esi
        leaq    .LC0(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
#APP
# 16 "inline_assembly.c" 1
        movl   -12(%rbp), %eax          
        subl   -8(%rbp), %eax          

# 0 "" 2
#NO_APP
        movl    %eax, -4(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, %esi
        leaq    .LC1(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align 8
        .long    1f - 0f
        .long    4f - 1f
        .long    5
0:
        .string  "GNU"
1:
        .align 8
        .long    0xc0000002
        .long    3f - 2f
2:
        .long    0x3
3:
        .align 8
4:

eax에다가 a의 값을 넣고 b의 값을 더하고 뺀 값을 각각 printf로 출력해주는 모습을 볼 수 있다. 사실 이걸 본 첫 의도는 Early Clobber의 유무로 달라지는 어셈블리 차이로 인한 결괏값의 오류를 보고 싶었는데, 어셈블리어에서 보이다시피 스택에서 값을 가져오니까 딱히 오류 날 일이 없어 보였다. 내가 본 래퍼런스에서는 저렇게 접근 안 하던데 gcc 버전의 차이가 아닐까 하는 생각이 든다.


추가적인 옵션이나 사용법은 아래 래퍼런스에서 저자가 잘 설명해놓았다. 필요할 때마다 찾아보도록 하자.

Reference

http://wiki.kldp.org/KoreanDoc/html/GCC_Inline_Assembly-KLDP/index.html

 

GCC inline assembly guide

이 문서는 gcc에서 inline assembly를 설명합니다. 설명이나 예제는 모두 ix86을 기준으로 하며 쓰이는 언어는 c입니다. GCC manual과 linux/freeBSD source 및 자신의 경험을 바탕으로 썼습니다. 사용방법정도

wiki.kldp.org

 

반응형