Linux Tutorial #7 비트 단위 연산

문연수·2021년 5월 19일
0

Linux Tutorial

목록 보기
8/25

6장에서 언급했듯이 long 자료형의 크기는 다른 자료형에 비해 훨씬 더 가변적이다. (C 의 모든 정수 기본 자료형의 크기는 앞서 말했듯 가변적이지만 아키텍처가 정해지면 그 크기는 고정된다. 하지만 long 은 같은 아키텍처라도 32-bit CPU 인지, 64-bit CPU 에 따라 그 크기가 바뀔 수 있다. 여기에서 필자가 말하는 아키텍처ISA 를 의미한다.

이러한 long 의 값을 확실히 하기 위해 리눅스 커널에서는 bitsperlong.h 헤더파일을 제공한다

1. bitsperlong: long 은 몇 비트?

bitsperlong.h

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __ASM_GENERIC_BITS_PER_LONG
#define __ASM_GENERIC_BITS_PER_LONG

#include "config.h"

// arch/x86/include/uapi/asm/bitsperlong.h
#if defined(__x86_64__) && !defined(__ILP32__)
# define __BITS_PER_LONG 64
#else
# define __BITS_PER_LONG 32
#endif

/*
// arch/arm64/include/uapi/asm/bitsperlong.h
# define __BITS_PER_LONG 64

// arch/riscv/include/uapi/asm/bitsperlong.h
# define __BITS_PER_LONG (__SIZEOF_POINTER__ * 8)

// include/uapi/asm-generic/bitsperlong.h
#ifndef __BITS_PER_LONG
#define __BITS_PER_LONG 32
#endif
 */

#ifdef CONFIG_64BIT
#define BITS_PER_LONG 64
#else
#define BITS_PER_LONG 32
#endif /* CONFIG_64BIT */

/*
 * FIXME: The check currently breaks x86-64 build, so it's
 * temporarily disabled. Please fix x86_64 and reenable
 */
#if 0 && BITS_PER_LONG != __BITS_PER_LONG
#error Inconsistent word size. Check asm/bitsperlong.h
#endif

#ifndef BITS_PER_LONG_LONG
#define BITS_PER_LONG_LONG 64
#endif

#endif /* __ASM_GENERIC_BITS_PER_LONG */

bitsperlong.h 코드는 각각의 아키텍처에 들어있는 bitsperlong.h 헤더의 내용을 발췌하여 통합한 것이다. 행 주석에 경로를 적어놨으니 직접 들어가서 확인하는 것도 도움이 될 것이다. 현재 필자의 환경은 x86_64 이기 때문에 RISC VARM64 는 주석처리 하였다. __x86_64__ 매크로는 컴파일러가 정의하는 상수값으로 x86 GCC 라면 자동으로 생성해준다. __ILP32__ 데이터 모델 중의 하나이다. 데이터의 크기를 정하는 집합인데 자세한 내용은 출처 를 참고하길 바란다.
마지막으로 BITS_PER_LONG 매크로의 값은 CONFIG_64BIT 에 따라 갈리는데 이는 config.h 에 정의되어 있다. 사실 CONFIG_64BIT 매크로는 make menuconfig 에서 설정하는 정보이지만 편의를 위해 필자는 config.h 포함하는 방법을 택했다.

2. config.h: long 의 크기를 정하는 헤더파일

config/config.h

/**
	M: Yeounsu Moon <yyyynoom@gmail.com>
	W: https://velog.io/@mythos
	F: config/config.h
	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Pulbic License
*/

#ifndef __CONFIG_H
#define __CONFIG_H

#define CONFIG_64BIT

#endif /* __CONFIG_H */

다음의 헤더파일을 포함하게 되면 CONFIG_64BIT 는 정의되므로 BITS_PER_LONG 의 값은 64 가 된다.

위 내용을 테스트하기 위한 코드인 test/bits/bitsperlong.c 를 작성해보겠다:

test/bits/bitsperlong.c

/**
	M: Yeounsoo Moon <yyyynoom@gmail.com>
	W: https://velog.io/@mythos
	F: test/bits/bitsperlong.c
	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
*/

#include <stdio.h>
#include <stdlib.h>

#include "type02.h"
#include "type03.h"

#include "bitsperlong.h"

int main(void)
{
#if defined(__x86_64__) && !defined(__ILP32__)
	printf("__x86_64__ defined\n");
#else
	printf("__x86_64__ NOT defined\n");
#endif
	printf("__BITS_PER_LONG = %d bits\n", BITS_PER_LONG);
	printf("BITS_PER_LONG = %d bits\n", BITS_PER_LONG);

	return 0;
}

여기까지 잘 따라왔다면 파일과 디렉토리가 다음과 같이 배치 되어야 한다. 무언가 빠진게 있다면 뒤로 돌아가서 하나씩 천천히 훑어보길 바란다.

각 파일에 위치를 컴파일러에게 알려서 다음과 같이 빌드한다:

 gcc test/bits/bitsperlong.c -I data/ -I config -I ./

실행결과는 위와 같다. __x86_64__ defined 도 제대로 출력 되었고 config.h 를 포함하였으므로 BITS_PER_LONG 의 값 역시 64 bits 로 나온다.

3. 파일 정리

한 두번은 그냥 저냥 작성할 수 있지만 컴파일할때마다 매번 여러 경로를 포함하는 것은 여간 불편한 일이 아니다. 따라서 헤더 파일들을 하나의 경로에 모아서 저장할 것이다.
뿔뿔히 흩어져 있던 파일을 새로운 디렉토리 include/linux 로 옮겼다. 명령어가 크게 줄진 않았으나 다음과 같이 하나의 폴더에 저장해 두는 것이 더 직관적이고 깔끔하며 이후 관리가 편하다. 만약 프로젝트가 더 커진다면 단순히 한군데에 모아 두는 것이 아닌 파일을 분류하여 거기에 맞는 makefile 을 작성하는 것이 맞다.

4. 단순 비트 연산

리눅스 커널에서는 다음과 같은 단순 비트 연산 매크로를 제공한다:

include/linux/bits.h

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __LINUX_BITS_H
#define __LINUX_BITS_H
#include "bitsperlong.h"

#define BIT(nr)			(1UL << (nr))
#define BIT_ULL(nr)		(1ULL << (nr))
#define BIT_MASK(nr)		(1UL << ((nr) % BITS_PER_LONG))
#define BIT_WORD(nr)		((nr) / BITS_PER_LONG)
#define BIT_ULL_MASK(nr)	(1ULL << ((nr) % BITS_PER_LONG_LONG))
#define BIT_ULL_WORD(nr)	((nr) / BITS_PER_LONG_LONG)
#define BITS_PER_BYTE		8

/*
 * Create a contiguous bitmask starting at bit position @l and ending at
 * position @h. For example
 * GENMASK_ULL(39, 21) gives us the 64bit vector 0x000000ffffe00000.
 */
#define GENMASK(h, l) \
	(((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h))))

#define GENMASK_ULL(h, l) \
	(((~0ULL) - (1ULL << (l)) + 1) & \
	 (~0ULL >> (BITS_PER_LONG_LONG - 1 - (h))))

#endif	/* __LINUX_BITS_H */

위 코드는 include/linux/bits.h 파일에서 그대로 가져와서 #include "bitsperlong.h" 부분만 수정한 코드이다. 로컬 경로 역시 커널 경로와 동일하게 지정했다. 비트 쉬프트, 비트 마스크 등의 단순한 기능을 parameterized macro 로 제공한다. 아래의 코드는 include/linux/bits.h 헤더 파일의 기능을 테스트 하는 코드이다:

test/bits/bits1.c

/**
	M: Yeounsoo Moon <yyyynoom@gmail.com>
	W: https://velog.io/@mythos
	F: test/bits/bits1.c
	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
*/

#include <stdio.h>
#include <stdlib.h>

#include "type02.h"
#include "type03.h"
#include "bits.h"

void bit64_print(u64 v)
{
	u64 mask = BIT_ULL(BITS_PER_LONG_LONG - 1);
	while (mask) {
		printf("%d", (v&mask ? 1 : 0));
		mask >>= 1;
	}

	printf("\n");
}

int main(void)
{
	u64 bits64;
	u32 i;

	for (i = 0; i < 256; i += 15) {
		bits64 = BIT_ULL(i);
		printf("BIT_ULL(%03d) = ", i);
		bit64_print(bits64);
		printf("BIT_ULL_WORD = %d\n", BIT_ULL_WORD(i));
	}

	bits64 = GENMASK_ULL(39, 21);
	printf("GENMASK_ULL() = 0x%016LX\n", bits64);
	printf("GENMASK_ULL() = ");
	bit64_print(bits64);
	
	return 0;
}

실행 결과는 다음과 같다:
BIT_ULL(x) 는 x - 1 번째 비트만 켜진 값을 반환한다. 재미있는 사실은 64 보다 크거나 같은 값 인 경우인데 이는 실제로 C 에서는 Undefined Behavior 이다. 당연히 피해야 하는 행동이지만 우리는 컴파일러와 아키텍처를 알고 있기 때문에 써도(사실 무조건 안 쓰는게 가장 좋다) 된다. 특이하게도 x86 에서는 넘어간 비트를 버리지 않고 2^n (nleft-side operand 의 비트 수)으로 나눈 값을 반환한다.
GENMASK_ULL(h, l) 은 첫 번째 인자부터 두 번째 인자 사이의 비트를 켠 값을 반환한다. 이 아이디어 역시 꽤나 일품이므로 매크로를 천천히 읽어보는 걸 추천한다.

5. bitops.h: 복합 비트 연산

리눅스 커널은 단순 비트 연산을 도와주는 매크로를 정의하는 bits.h 외에도 복잡한 비트 연산을 돕는 bitops.h 헤더를 정의한다.

include/linux/bitops.h

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_BITOPS_H
#define _LINUX_BITOPS_H

#include "types.h"
#include "bits.h"

#define BITS_PER_TYPE(type) (sizeof(type) * BITS_PER_BYTE)
#define BITS_TO_LONGS(nr)	DIV_ROUND_UP(nr, BITS_PER_TYPE(long))

static inline __u64 rol64(__u64 word, unsigned int shift)
{
	return (word << shift) | (word >> (64 - shift));
}

static inline __u64 ror64(__u64 word, unsigned int shift)
{
	return (word >> shift) | (word << (64 - shift));
}

static inline __u32 rol32(__u32 word, unsigned int shift)
{
	return (word << shift) | (word >> ((-shift) & 31));
}

static inline __u32 ror32(__u32 word, unsigned int shift)
{
	return (word >> shift) | (word << (32 - shift));
}

static inline __u16 rol16(__u16 word, unsigned int shift)
{
	return (word << shift) | (word >> (16 - shift));
}

static inline __u16 ror16(__u16 word, unsigned int shift)
{
	return (word >> shift) | (word << (16 - shift));
}

static inline __u8 rol8(__u8 word, unsigned int shift)
{
	return (word << shift) | (word >> (8 - shift));
}

static inline __u8 ror8(__u8 word, unsigned int shift)
{
	return (word >> shift) | (word << (8 - shift));
}

static inline __s32 sign_extend32(__u32 value, int index)
{
	__u8 shift = 31 - index;
	return (__s32)(value << shift) >> shift;
}

static inline __s64 sign_extend64(__u64 value, int index)
{
	__u8 shift = 63 - index;
	return (__s64)(value << shift) >> shift;
}

#ifdef __KERNEL__

#ifndef set_mask_bits
#define set_mask_bits(ptr, mask, bits)				\
({								\
	const typeof(*(ptr)) mask__ = (mask), bits__ = (bits);	\
	typeof(*(ptr)) old__, new__;				\
								\
	do {							\
		old__ = READ_ONCE(*(ptr));			\
		new__ = (old__ & ~mask__) | bits__;		\
	} while (cmpxchg(ptr, old__, new__) != old__);		\
								\
	new__;							\
})
#endif

#ifndef bit_clear_unless
#define bit_clear_unless(ptr, clear, test)			\
({								\
	const typeof(*(ptr)) clear__ = (clear), test__ = (test);\
	typeof(*(ptr)) old__, new__;				\
								\
	do {							\
		old__ = READ_ONCE(*(ptr));			\
		new__ = old__ & ~clear__;			\
	} while (!(old__ & test__) &&				\
		 cmpxchg(ptr, old__, new__) != old__);		\
								\
	!(old__ & test__);					\
})
#endif

#ifndef find_last_bit
*/
extern unsigned long find_last_bit(const unsigned long *addr,
				   unsigned long size);

#endif /* __KERNEL__ */
#endif

위 코드는 리눅스 커널의 include/linux/bitops.h 헤더를 긁어서, 불필요한 기능은 제거한 버전이다. 실제 코드에는 주석도 달려 있었으나 너무 루즈해지는 것을 막기 위해 주석을 모두 지웠다. 5 행types.htype02.htype03.h 를 순서대로 포함하는 내용을 담고 있는 헤더파일이다. 함수에 대한 소개는 하지 않고 간단한 예제를 작성하고 거기에서 짧게 설명토록 하겠다. 실제 리눅스 커널의 코드에선 함수의 기능을 모두 주석으로 적어 놨으므로 그걸 읽어보길 바란다.

include/linux/bits2.c

/**
	M: Yeounsoo Moon <yyyynoom@gmail.com>
	W: https://velog.io/@mythos
	F: test/bits/bits2.c
	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
*/

#include <stdio.h>
#include <stdlib.h>

#include "type02.h"
#include "type03.h"
#include "bits.h"
#include "bitops.h"

void bit64_print(u64 v)
{
	u64 mask = BIT_ULL(BITS_PER_LONG_LONG - 1);
	while (mask) {
		printf("%d", (v & mask ? 1 : 0));
		mask >>= 1;
	}

	printf("\n");
}

void bit32_print(u32 v)
{
	u32 mask = BIT_ULL(BITS_PER_TYPE(v) - 1);
	while (mask) {
		printf("%d", (v & mask ? 1 : 0));
		mask >>= 1;
	}

	printf("\n");
}

int main(void)
{
	u64 d64 = 0xF000000000000000ULL;
	u64 r64;

	r64 = rol64(d64, 3);
	printf("rol64(0x%016LX, 3):\n", d64);
	bit64_print(r64);

	u32 d32 = 0x0F000000U;
	u32 r32;

	r32 = rol32(d32, 7);
	printf("rol32(0x%08X, 7):\n", d32);
	bit32_print(d32);
	return 0;
}

보는 것과 같이 rol32rol64 함수는 비트를 회전 시킨다. 범위를 넘어간 비트를 버리지 않고 밀려서 빈 자리를 다시 채운다.

6. 정리

지금까지의 내용을 순서대로 잘 따라왔다면 다음과 같이 파일과 디렉토리가 구성되어야 한다:
다시 한번 얘기하지만 types.h 는 그저 type02type03 을 그저 순서대로 포함하는 내용을 가지는 헤더 파일이다. include guard 는 넣지 않았다.

출처

[책] 리눅스 커널 소스 해설: 기초입문 (정재준 저)
[사이트] https://docs.oracle.com/cd/E19620-01/805-3024/lp64-1/index.html#:~:text=The%2032%2Dbit%20Solaris%20C,and%20char%20as%208%20bits.
[사이트] http://archive.opengroup.org/public/tech/aspen/lp64_wp.htm
[사이트] https://kr.mathworks.com/help/fixedpoint/ug/use-of-shifts-by-c-coders.html
[책] C: A Reference Manual (Fifth Edition) (Samuel P. Harbison III, Guy L. Steele Jr.)

profile
2000.11.30

0개의 댓글