LLVM의 IR 명령어에 대한 자세한 설명

저자는 Android 보안 분야에 중점을 둡니다.내 개인 WeChat 공개 계정 " Android 보안 엔지니어링 "에 관심을 가져 주셔서 감사합니다. 개인 WeChat 공개 계정은 주로 Android 애플리케이션의 보안 보호 및 역 분석에 중점을 두고 다양한 보안 공격 및 방어 방법, Hook 기술, ARM 컴파일 및 기타 Android 관련 지식을 공유합니다.

건의: 이 글은 내용이 많으니 저장해 두었다가 나중에 필요할 때 참고 매뉴얼로 활용하는 것을 추천합니다. 일반적으로 IR 명령어는 특정 명령어가 있다는 것만 알면 되며, 이를 암기하는 데 시간을 들일 필요가 없습니다.

개요

IR 명령은 프로그램의 제어 흐름, 데이터 흐름, 메모리 액세스 등을 나타내는 데 사용되는 LLVM의 중간 표현으로 SSA 형식(Static Single Assignment)을 기반으로 하는 정적 단일 할당 형식입니다. LLVM에서 각 IR 명령에는 명령 유형을 식별하는 데 사용되는 고유한 opcode(opcode)가 있으며 각 opcode는 가능한 피연산자(operands) 집합에 해당하며 이는 상수, 레지스터 또는 다른 결과일 수 있습니다. 지침.

LLVM의 IR에서 모든 데이터 타입은 LLVM 타입 시스템을 기반으로 정의되며, 이러한 데이터 타입에는 정수, 부동 소수점 숫자, 포인터, 배열, 구조체 등이 포함되며 각 데이터 타입은 비트 폭과 같은 고유한 속성을 가집니다. , 정렬 등이 있습니다. IR에서 각 값에는 명시적으로 지정하거나 명령의 피연산자에서 추론할 수 있는 유형이 있습니다.

LLVM의 IR 명령어는 산술, 논리, 비교, 변환, 제어 흐름 등을 포함하여 매우 풍부하며 복잡한 프로그램 구조를 표현하는 데 사용할 수 있으며 IR 명령어는 LLVM의 옵티마이저로 최적화하여 효율적인 대상 코드를 생성할 수도 있습니다.

많은 유형의 IR 명령어가 있으며 다음은 몇 가지 일반적인 명령어 유형입니다.

  1. 더하기, 빼기, 곱하기 및 나누기 명령어: add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv 등
  2. 비트 연산 명령어: and, or, xor, shl, lshr, ashr 등
  3. 변환 지침: trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast 등
  4. 메모리 명령어: alloca, load, store, getelementptr 등
  5. 흐름 제어 명령: br, switch, ret, indirectbr, invoke, resume, unreachable 등
  6. 기타 명령: phi, select, call, va_arg, landingpad 등

더하기, 빼기, 곱하기 및 나누기 명령

1. 더하기 명령어(add)

덧셈 명령은 두 값을 더하는 데 사용됩니다. LLVM에서 더하기 명령어의 구문은 다음과 같습니다.

%result = add <type> <value1>, <value2>

그 중에서 <type>더할 값의 데이터 유형을 나타내는 정수, 부동 소수점 숫자 등이 될 수 있고, <value1>더할 <value2>두 개의 숫자를 나타내는 상수, 레지스터 또는 다른 명령어의 결과가 될 수 있습니다.

LLVM에서 add명령어의 매개변수는 및 유형 유형을 <type>지정합니다 . 지원되는 유형은 다음과 같습니다.<value1><value2><result>

  • 정수 유형: i1, i8, i16, i32, i64i128.
  • 부동 소수점 유형: , half, float.doublefp128
  • 벡터 유형: <n x i8>, <n x i16><n x i32>;
  • 포인터 유형: i8*, i32*float*;
  • 태그 유형: metadata;

예를 들어 두 개의 정수를 더하고 정수 결과를 얻으려면 다음 명령을 사용할 수 있습니다.

%result = add i32 1, 2

여기서 <type>로 지정된 i32<value1>정수 값 1, <value2>는 정수 값 2, <result>은 정수 유형 입니다 i32. 다양한 유형의 메모리 공간 크기(비트 단위)는 다음과 같습니다.

  • 정수형: i11비트, i88비트, i1616비트, i3232비트, i6464비트, i128128비트;
  • 부동 소수점 유형: half16비트, float32비트, double64비트, fp128128비트;
  • 벡터 유형: <n x i8>자리 표시자n * 8 , <n x i16>자리 표시자n * 16 , <n x i32>자리 표시자n * 32
  • 포인터 유형: 포인터 유형의 크기는 런타임 시 운영 체제 및 아키텍처에 따라 다릅니다.예를 들어 32비트 운영 체제에서는 포인터 유형이 일반적으로 4바이트(32비트)를 차지하며 64비트 운영 체제에서는 , 포인터 유형은 일반적으로 8바이트를 차지합니다.바이트(64비트);
  • 태그 유형: metadata유형은 일반적으로 포인터 유형과 동일한 공간을 차지합니다.

여기에는 LLVM에서 다양한 유형의 기본 크기만 있다는 점에 유의해야 합니다. 사실 LLVM IR을 사용할 때 개발자는 유형 뒤에 숫자를 추가하여 유형의 크기를 명시적으로 지정할 수 있습니다. 예를 들어 유형은 Pass일 수 있습니다 i16. i16 12316비트 정수 값을 나타냅니다 123.

다음은 두 개의 정수를 더하는 더하기 명령의 코드 예입니다.

%x = add i32 2, 3

이 명령어는 상수 2와 를 더하고 3결과를 레지스터에 저장합니다 %x.

상수 외에도 레지스터 또는 다른 명령어의 결과를 추가 명령어의 피연산자로 사용할 수도 있습니다. 예를 들면 다음과 같습니다.

%x = add i32 %a, %b
%z = add i32 %x, %y

코드의 첫 번째 줄은 레지스터 %a및 에 값을 더하고 %b결과를 레지스터에 저장하고 %x두 번째 코드 줄은 레지스터 %x및 에 값을 더하고 %y결과를 레지스터에 저장합니다 %z.

LLVM에서는 carry( add with carry)를 사용한 덧셈 명령어와 overflow( add with overflow)를 사용한 덧셈 명령어도 지원하므로 여기서는 반복하지 않겠습니다.

2. 빼기 명령어(sub)

빼기 명령은 두 값을 빼는 데 사용되며 구문은 다음과 같습니다.

%result = sub <type> <value1>, <value2>

그 중에서 <type>빼려는 값의 데이터 유형을 나타내며 정수, 부동 소수점 숫자 등이 될 수 있고 <value1>빼려 <value2>는 두 숫자는 상수, 레지스터 또는 다른 명령어의 결과가 될 수 있습니다.

다음은 두 정수를 빼는 빼기 명령어의 코드 예입니다.

%diff = sub i32 %x, %y

이 명령어는 레지스터 의 값에서 레지스터 %x의 값을 빼고 그 결과를 레지스터에 저장합니다 .%y%diff

두 개의 부동 소수점 숫자 사이의 차이를 계산하는 데 사용할 수 있는 빼기 명령어 형식도 있습니다. 구문은 다음과 같습니다.

%result = fsub <type> <value1>, <value2>

그 중 <type>빼려는 값을 나타내는 데이터 타입은 부동 소수점 숫자 타입이어야 하며, 빼려는 두 숫자를 나타내야 하며 상수, 레지스터 또는 다른 명령어의 결과일 수 있습니다 <value1>.<value2>

다음은 두 개의 단정밀도 부동 소수점 숫자를 빼는 부동 소수점 빼기 명령어의 코드 예제입니다.

%diff = fsub float %x, %y

이 명령어는 register %x의 단정밀도 부동 소수점 숫자 %y에서 register 의 단정밀도 부동 소수점 숫자를 빼고 결과를 register 에 저장합니다 %diff.

3. 곱셈 명령어(mul)

곱셈 명령어는 두 값을 곱하는 데 사용되며 구문은 다음과 같습니다.

%result = mul <type> <value1>, <value2>

그 중 <type>곱할 값의 데이터 유형을 나타내는 정수, 부동 소수점 숫자 등이 될 수 있고, <value1>곱할 <value2>두 숫자를 나타내는 상수, 레지스터 또는 다른 명령어의 결과가 될 수 있습니다.

다음은 두 정수를 곱하는 곱셈 명령의 코드 예입니다.

%prod = mul i32 %x, %y

이 명령어는 레지스터 의 값을 곱하고 %x레지스터 에 결과를 저장합니다. 다음과 같이 부동 소수점 숫자에 대해 곱셈 연산을 수행할 수도 있습니다.%y%prod

%result = mul double %value1, %value2

이 명령어는 레지스터 %value1%value2의 값을 곱하고 결과 %result를 에 저장합니다. 부동 소수점 숫자의 곱셈 연산의 경우 또는 와 같은 부동 소수점 유형을 사용해야 합니다 double.float

또한 LLVM은 벡터 곱셈 명령, 부호 없는 정수 곱셈 명령 등과 같은 다른 유형의 곱셈 명령도 제공합니다. 구체적인 명령어 사용 방법은 LLVM의 공식 문서를 참조하세요.

4. 나눗셈 명령어(div)

나누기 명령은 두 값을 나누는 데 사용되며 구문은 다음과 같습니다.

%result = <s/u>div <type> <value1>, <value2>

여기서, 表示要执行有符号(`sdiv`)还是无符号(`udiv`)的除法运算;나눌 값을 나타내는 데이터 유형은 정수, 부동 소수점 숫자 등이 될 수 있으며 각각 나눌 두 숫자를 나타내며 상수, 레지스터 또는 다른 명령의 결과일 수 있습니다.

다음은 두 정수를 나누는 나누기 명령어의 코드 예입니다.

%quot = sdiv i32 %x, %y

이 명령어는 레지스터 %x의 값을 레지스터 %y의 값으로 나누고 그 결과를 레지스터에 저장합니다 %quot. 명령어를 사용하므로 sdiv부호 있는 나누기 연산이 수행됩니다.

부호 없는 나눗셈을 하려면 udiv다음 명령을 사용할 수 있습니다.

%quot = udiv i32 %x, %y

이 명령어는 레지스터 %x의 값을 레지스터 %y의 값으로 나누고 그 결과를 레지스터에 저장합니다 %quot. udiv명령 으로 인해 부호 없는 나눗셈 연산이 수행됩니다.

비트 연산 명령

IR에는 비트 and(and), 비트 or(or), 비트 배타적 or(xor), 비트 반전(not) 등 다양한 비트 연산 명령어가 있습니다. 이러한 명령어는 정수 유형에 대해 비트 연산을 수행하고 결과를 새 레지스터에 저장합니다. 다음은 일반적인 비트 연산 명령어와 IR에서의 기능입니다.

  1. 비트 AND(and): 두 정수의 이진 표현에 대해 비트 AND 연산을 수행합니다.
  2. 비트 OR(또는): 두 정수의 이진 표현에 대해 비트 OR 연산을 수행합니다.
  3. 비트 XOR(xor): 두 정수의 이진 표현에 대해 비트 XOR 연산을 수행합니다.
  4. 비트 반전(not): 정수의 이진 표현에 대해 비트 반전 연산을 수행합니다.

<type>이러한 명령어 는 i1, i8, i16, i32, i64 <value1>등 이 될 수 있는 비트 연산의 대상이 되는 정수의 데이터 유형을 나타내는 유사한 구문으로 사용될 수 있습니다 <value2>. 레지스터 또는 다른 명령어의 결과. 예를 들어:

%result = and i32 %x, %y
%result = or i32 %x, %y
%result = xor i32 %x, %y
%result = xor i32 %x, -1

첫 번째 명령은 와 비트 AND 연산을 수행하고 결과를 에 저장하고 %x, 두 번째 명령은 와 비트 OR 연산을 수행 하여 결과를 에 저장하고, 세 번째 명령은 및 와 비트 XOR 연산을 수행하고 결과를 에 저장합니다 . 이진수에서 모두 1인 숫자, 즉 의 각 비트를 반전하여 비트별 XOR 연산을 수행하고 결과를 에 저장합니다.%y%result%x%y%result%x%y%result%x%x%result

변환 명령

  1. trunc: 정수 또는 부동 소수점 숫자를 더 작은 자릿수로 자릅니다. 즉, 일부 상위 이진 비트를 제거합니다.
  2. zext: 정수 또는 부울 값의 비트 수를 늘리고 새 비트 수의 상위 비트를 0으로 채웁니다. 즉, 제로 확장입니다.
  3. sext: 정수의 자릿수를 늘리면 새로운 자릿수의 상위 비트가 원래의 최상위 자릿수로 채워집니다. 즉, 부호 확장이 수행됩니다.
  4. fptrunc: 부동 소수점 숫자를 더 작은 자릿수로 자릅니다. 즉, 일부 상위 이진수를 제거합니다. 반올림 작업이므로 일부 정밀도가 손실될 수 있습니다.
  5. fpext: 부동 소수점 숫자의 비트 수를 늘리고 새 숫자의 상위 비트를 0으로 채웁니다. 즉, 부동 소수점 0 확장을 수행합니다.
  6. fptoui: 부동 소수점 숫자를 부호 없는 정수로 변환합니다. float가 음수이면 결과는 0입니다.
  7. fptosi: 부동 소수점 숫자를 부호 있는 정수로 변환합니다. float가 음수이면 결과는 음수인 가장 작은 정수입니다.
  8. uitofp: 부호 없는 정수를 부동 소수점 숫자로 변환합니다.
  9. sitofp: 부호 있는 정수를 부동 소수점 숫자로 변환합니다.
  10. ptrtoint: 포인터 유형을 정수 유형으로 변환합니다. 이 명령어는 일반적으로 계산을 위해 포인터를 정수로 변환하는 데 사용됩니다.
  11. inttoptr: 정수형을 포인터형으로 변환합니다. 이 명령어는 일반적으로 메모리 주소 계산을 위해 정수를 포인터로 변환하는 데 사용됩니다.
  12. bitcast: 값을 한 유형에서 다른 유형으로 변환하지만 유형의 비트 수는 동일해야 합니다. 이 명령어는 비트 연산을 위해 부동 소수점 숫자를 정수로 변환하는 것과 같은 저수준 메모리 연산을 구현하는 데 사용할 수 있습니다.

다음은 IR 변환 명령의 자세한 사용 지침과 예입니다.

1. 트렁크

trunc이 명령어는 정수 또는 부동 소수점 숫자를 원래보다 작은 비트 수로 자릅니다. 즉, 일부 상위 비트를 제거합니다. trunc명령의 사용 형식은 다음과 같습니다.

%result = trunc <source type> <value> to <destination type>

그 중 <source type>and는 <destination type>각각 소스 타입과 타겟 타입을 나타내며, <value>변환할 값을 나타냅니다. 예를 들어 다음 코드는 64비트 정수를 32비트 정수로 자릅니다.

%long = add i64 1, 2
%short = trunc i64 %long to i32

이 예에서 는 %long값이 3(1+2)인 64비트 정수입니다. %short값이 3인 32비트 정수입니다. 32비트 정수로 잘려서 %long하위 32비트 값만 남습니다.

2. 압력

zext명령은 정수 또는 부울 값의 비트 수를 늘리고 새 비트 수의 상위 비트는 0으로 채워집니다. 즉, 0 확장됩니다. zext명령의 사용 형식은 다음과 같습니다.

%result = zext <source type> <value> to <destination type>

예를 들어, 다음 코드는 8비트 정수를 16비트 정수로 확장합니다.

%short = add i8 1, 2
%long = zext i8 %short to i16

이 예에서 는 %short값이 3(1+2)인 8비트 정수입니다. %long값이 3인 16비트 정수입니다. %short16비트 정수로 확장되므로 상위 8비트는 제로 패딩됩니다 .

3.섹스

sext이 명령어는 정수의 비트 수를 증가시키고 새로운 비트 수의 상위 비트를 원래의 상위 비트로 채우는 것, 즉 부호 확장을 수행합니다. 지시어는 지시어와 유사한 sext형식으로 사용됩니다 .zext

%result = sext <source type> <value> to <destination type>

예를 들어, 다음 코드는 8비트 정수를 16비트 정수로 확장합니다.

%short = add i8 -1, 2
%long = sext i8 %short to i16

이 예에서 는 %short값이 1-2=-1인 8비트 정수입니다. %long값이 0xffff인 16비트 정수입니다. %short16비트 정수로 확장되기 때문에 상위 8비트는 모두 1로 채워진다.

4.fptrunc

fptrunc이 명령어는 부동 소수점 숫자를 원래보다 작은 비트 수로 자릅니다. 즉, 일부 상위 이진 비트를 제거합니다. fptrunc명령의 사용 형식은 다음과 같습니다.

%result = fptrunc <source type> <value> to <destination type>

예를 들어 다음 코드는 배정밀도 부동 소수점 숫자를 단정밀도 부동 소수점 숫자로 자릅니다.

%double = fadd double 1.0, 2.0
%float = fptrunc double %double to float

이 예에서 는 %double값이 3.0(1.0+2.0)인 배정밀도 부동 소수점 숫자입니다. %float값이 3.0인 단정밀도 부동 소수점 숫자입니다. %double정밀도 부동 소수점 숫자로 잘리므로 상위 값은 잘리고 하위 값만 남습니다.

5.fpext

fpext이 명령은 부동 소수점 숫자를 더 큰 자릿수로 확장하고 새 자릿수의 상위 비트는 0으로 채워집니다. 지시어는 지시어와 유사한 fpext형식으로 사용됩니다 .fptrunc

%result = fpext <source type> <value> to <destination type>

예를 들어 다음 코드는 단정밀도 부동 소수점 숫자를 배정밀도 부동 소수점 숫자로 확장합니다.

%float = fadd float 1.0, 2.0
%double = fpext float %float to double

이 예에서는 %float값이 3.0(1.0+2.0)인 단정밀도 부동 소수점 숫자입니다. %double값이 3.0인 배정밀도 부동 소수점 숫자입니다. 배정밀도 부동 소수점 숫자로 확장된 결과 %float새로운 상위 비트는 모두 0으로 채워집니다.

6.fptoui

fptoui이 명령어는 부동 소수점 숫자를 부호 없는 정수로 변환합니다. 변환할 때 float 값이 음수이면 결과는 0입니다. fptoui명령의 사용 형식은 다음과 같습니다.

%result = fptoui <source type> <value> to <destination type>

예를 들어 다음 코드는 배정밀도 부동 소수점 숫자를 32비트 부호 없는 정수로 변환합니다.

%double = fadd double 1.0, 2.0
%uint = fptoui double %double to i32

이 예에서 는 %double값이 3.0(1.0+2.0)인 배정밀도 부동 소수점 숫자입니다. %uint값이 3인 32비트 부호 없는 정수입니다. 의 값이 양수 이므로 %double32비트 부호 없는 정수로 변환할 수 있습니다.

7. 안검하수

fptosi이 명령어는 부동 소수점 숫자를 부호 있는 정수로 변환합니다. 변환할 때 부동 소수점 숫자의 값이 대상 유형의 표현 가능한 범위를 벗어나면 결과는 해당 유형의 최소값 또는 최대값입니다. fptosi명령의 사용 형식은 다음과 같습니다.

%result = fptosi <source type> <value> to <destination type>

예를 들어 다음 코드는 배정밀도 부동 소수점 숫자를 32비트 부호 있는 정수로 변환합니다.

%double = fadd double 1.0, -2.0
%i32 = fptosi double %double to i32

이 예에서 는 %double값이 -1.0(1.0-2.0)인 배정밀도 부동 소수점 숫자입니다. %i32값이 -1인 32비트 부호 있는 정수입니다. %double의 값이 음수 이므로 32비트 부호 있는 정수로 변환할 수 있습니다.

8.오프오프

uitofp이 명령어는 부호 없는 정수를 부동 소수점 숫자로 변환합니다. uitofp명령의 사용 형식은 다음과 같습니다.

%result = uitofp <source type> <value> to <destination type>

예를 들어 다음 코드는 32비트 부호 없는 정수를 단정밀도 부동 소수점 숫자로 변환합니다.

%uint = add i32 1, 2
%float = uitofp i32 %uint to float

이 예에서 는 %uint값이 3인 32비트 부호 없는 정수입니다. %float값이 3.0인 단정밀도 부동 소수점 숫자입니다. 값이 양수 이므로 %uint단정밀도 부동 소수점 숫자로 변환할 수 있습니다.

9.시토프

sitofp이 명령어는 부호 있는 정수를 부동 소수점 숫자로 변환합니다. sitofp명령의 사용 형식은 다음과 같습니다.

%result = sitofp <source type> <value> to <destination type>

예를 들어, 다음 코드는 32비트 부호 있는 정수를 단정밀도 부동 소수점 숫자로 변환합니다.

%i32 = add i32 1, -2
%float = sitofp i32 %i32 to float

이 예에서 는 %i32값이 -1인 32비트 부호 있는 정수입니다. %float값이 -1.0인 단정밀도 부동 소수점 숫자입니다. 의 값이 음수 이므로 %i32단정밀도 부동 소수점 숫자로 변환할 수 있습니다.

10.ptrtoint

ptrtoint이 명령은 포인터 유형을 정수 유형으로 변환합니다. ptrtoint명령의 사용 형식은 다음과 같습니다.

%result = ptrtoint <source type> <value> to <destination type>

예를 들어 다음 코드는 포인터 유형을 64비트 정수 유형으로 변환합니다.

%ptr = alloca i32
%i64 = ptrtoint i32* %ptr to i64

이 예에서는 %ptr32비트 정수 유형에 대한 포인터입니다. 값이 포인터의 주소인 %i6464비트 정수 유형입니다 . 포인터형과 정수형은 비트폭이 다르기 때문에 형변환 명령어를 %ptr사용해야 합니다 .ptrtoint

11.inttoptr

inttoptr이 명령은 정수 유형을 포인터 유형으로 변환합니다. inttoptr명령의 사용 형식은 다음과 같습니다.

%result = inttoptr <source type> <value> to <destination type>

예를 들어, 다음 코드는 64비트 정수 유형을 32비트 정수 유형에 대한 포인터로 변환합니다.

%i64 = add i64 1, 2
%ptr = inttoptr i64 %i64 to i32*

이 예에서 는 %i64값이 3인 64비트 정수 유형입니다. %ptr값이 3인 32비트 정수 유형에 대한 포인터입니다. 정수형과 포인터형은 비트폭이 다르기 때문에 inttoptr형 변환을 위한 명령어가 필요하다.

12.비트캐스트

bitcast명령어는 값의 비트 표현을 다른 유형으로 변환하지만 값 자체를 변경하지는 않습니다. bitcast명령의 사용 형식은 다음과 같습니다.

%result = bitcast <source type> <value> to <destination type>

예를 들어, 다음 코드는 64비트 배정밀도 부동 소수점 숫자를 64비트 정수 유형으로 변환합니다.

%double = fadd double 1.0, -2.0
%i64 = bitcast double %double to i64

이 예에서 는 %double값이 -1.0(1.0-2.0)인 64비트 배정밀도 부동 소수점 숫자입니다. %i64는 64비트 정수형이고 값은 0xbff8000000000000(-4616189618054758400)입니다. 배정도 부동 소수점 숫자와 64비트 정수 유형은 비트 폭이 같기 때문에 bitcast유형 변환에 명령어를 사용할 수 있습니다.

메모리 명령

LLVM IR은 alloca, load, store, getelementptr, malloc, free, 을 포함하여 몇 가지 일반적인 메모리 명령을 제공합니다 . 이러한 명령어는 메모리 할당, 초기화 및 복사 작업에 사용할 수 있습니다. 이러한 각 지침은 해당 코드 예제와 함께 아래에 설명되어 있습니다.memsetmemcpymemmove

1.할로카

alloca스택에 메모리를 할당하고 새로 할당된 메모리에 대한 포인터를 반환하라는 명령. alloca명령의 사용 형식은 다음과 같습니다.

%ptr = alloca <type>

여기서 <type>는 할당할 메모리 블록 유형입니다. 예를 들어, 다음 코드는 5개의 정수 배열을 할당합니다.

%array = alloca [5 x i32]

2.로드

load명령어는 메모리에서 데이터를 읽고 레지스터에 로드하는 데 사용됩니다. load명령의 사용 형식은 다음과 같습니다.

%val = load <type>* <ptr>

여기서 <type>는 읽을 데이터 유형이고 <ptr>데이터를 읽을 메모리 블록에 대한 포인터입니다. 예를 들어 다음 코드는 정수 배열의 첫 번째 요소를 레지스터로 로드합니다.

%array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
%val = load i32, i32* %ptr

이 예에서 는 %array정수 배열이고 는 %ptr배열의 첫 번째 요소에 대한 포인터이며 load명령어는 %ptr지시된 메모리 블록의 데이터를 %val레지스터로 로드합니다.

3.스토어

store명령어는 레지스터에서 메모리로 데이터를 쓰는 데 사용됩니다. store명령의 사용 형식은 다음과 같습니다.

store <type> <val>, <type>* <ptr>

여기서 는 <type>쓸 데이터의 유형, <val>는 쓸 데이터의 값, 는 <ptr>데이터를 쓸 메모리 블록에 대한 포인터입니다. 예를 들어 다음 코드는 정수 배열의 첫 번째 요소에 정수를 저장합니다.

%array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
store i32 42, i32* %ptr

이 예에서 %array는 정수 배열이고 는 %ptr배열의 첫 번째 요소에 대한 포인터이며 store명령어는 정수 값 42를 %ptr가리키는 메모리 블록에 저장합니다.

4.getelementptr

getelementptr명령어는 메모리의 데이터에 액세스하기 위해 포인터에서 오프셋을 계산하는 데 사용됩니다. getelementptr명령의 사용 형식은 다음과 같습니다.

%ptr = getelementptr <type>, <type>* <ptr>, <index type> <idx>, ...

그 중 는 <type>포인터가 가리키는 데이터 타입, <ptr>는 데이터에 대한 포인터, <index type>는 인덱스의 타입, <idx>는 인덱스의 값입니다. getelementptr지시문은 여러 인덱스를 허용할 수 있으며 각 인덱스는 모든 유형일 수 있습니다. 인덱스 유형은 오프셋을 계산하는 데 사용되는 정수 유형이어야 합니다. 예를 들어 다음 코드는 2차원 배열의 요소에 대한 포인터를 계산합니다.

%array = alloca [3 x [4 x i32]]
%ptr = getelementptr [3 x [4 x i32]], [3 x [4 x i32]]* %array, i32 1, i32 2

이 예에서 는 두 번째 행과 세 번째 열의 요소에 대한 포인터인 %array2차원 배열입니다 .%ptr

5.malloc

malloc힙에 메모리를 할당하고 새로 할당된 메모리에 대한 포인터를 반환하는 명령. malloc명령의 사용 형식은 다음과 같습니다.

%ptr = call i8* @malloc(i64 <size>)

여기서 는 <size>할당할 메모리 블록의 크기입니다. 예를 들어, 다음 코드는 10개의 정수 배열을 할당합니다.

%size = mul i64 10, i64 4
%ptr = call i8* @malloc(i64 %size)
%array = bitcast i8* %ptr to i32*

이 예에서 는 %size10개의 정수가 차지하는 바이트 수이고, call명령은 malloc메모리를 할당하는 함수를 호출하고, %ptr는 새로 할당된 메모리 블록에 대한 포인터이며, bitcast명령은 %ptr포인터를 정수 포인터 유형으로 변환합니다.

6. 무료

free지시문은 이전에 지시문에 의해 할당된 메모리를 해제하는 데 사용됩니다 malloc. free명령의 사용 형식은 다음과 같습니다.

call void @free(i8* <ptr>)

여기서 <ptr>해제할 메모리 블록에 대한 포인터입니다. 예를 들어 다음 코드는 이전에 할당된 정수 배열을 해제합니다.

%ptr = bitcast i32* %array to i8*
call void @free(i8* %ptr)

이 예제에서 이것은 포인터를 유형의 포인터 로 변환하는 명령어 %array에 의해 이전에 할당된 malloc정수 배열에 대한 포인터 이며 bitcast명령어는 메모리를 해제하는 함수를 호출합니다 .%arrayi8*callfree

7.멤셋

memset이 명령은 메모리 영역의 내용을 지정된 값으로 설정하는 데 사용됩니다. 기본 구문은 다음과 같습니다.

call void @llvm.memset.p0i8.i64(i8* %dst, i8 %val, i64 %size, i1 0)

그 중 첫 번째 파라미터는 %dst설정할 메모리 영역의 시작 주소로 포인터 타입이어야 한다. 두 번째 매개변수 %val는 설정할 값으로 정수여야 합니다. 세 번째 매개변수 %size는 메모리 영역의 크기이며 64비트 정수여야 합니다. 마지막 매개변수는 정렬을 나타내는 부울입니다. 1이면 포인터 타입에 따라 정렬됨을 의미하고, 0이면 포인터 타입에 따라 정렬되지 않음을 의미합니다.

다음은 정수 배열의 모든 요소를 ​​0으로 설정하는 간단한 사용 예입니다.

define void @set_to_zero(i32* %array, i32 %size) {
entry:
  %zero = alloca i32, align 4
  store i32 0, i32* %zero, align 4
  %array_end = getelementptr i32, i32* %array, i32 %size
  call void @llvm.memset.p0i8.i64(i8* %array, i8 0, i64 sub(i32* %array_end, %array), i1 false)
  ret void
}

8.memcpy

memcpy명령은 한 메모리 영역의 내용을 다른 메모리 영역으로 복사하는 데 사용됩니다. 기본 구문은 다음과 같습니다.

call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 %size, i1 0)

그 중 첫 번째 파라미터는 %dst대상 메모리 영역의 시작 주소로 포인터 타입이어야 한다. 두 번째 매개변수는 %src포인터 유형이어야 하는 소스 메모리 영역의 시작 주소입니다. 세 번째 매개변수 %size는 메모리 영역의 크기이며 64비트 정수여야 합니다. 마지막 매개변수는 정렬을 나타내는 부울입니다. 1이면 포인터 타입에 따라 정렬됨을 의미하고, 0이면 포인터 타입에 따라 정렬되지 않음을 의미합니다.

다음은 정수 배열을 다른 배열로 복사하는 간단한 사용 예입니다.

define void @copy_array(i32* %src, i32* %dst, i32 %size) {
entry:
  %src_end = getelementptr i32, i32* %src, i32 %size
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 sub(i32* %src_end, %src), i1 false)
  ret void
}

9.멤무브

memmove 명령은 소스 주소가 가리키는 메모리 블록의 데이터를 대상 주소가 가리키는 메모리 블록으로 이동하는 데 사용되며 그 정의는 다음과 같습니다.

declare void @llvm.memmove.p0i8.p0i8.i32(i8* nocapture, i8* nocapture, i32, i32, i1)

이 명령은 대상 주소, 소스 주소, 복사할 바이트 수, 정렬 및 중첩 검사용 플래그 등 5개의 매개변수를 허용합니다. 그 중 얼라인먼트 파라미터는 메모리 블록의 얼라인먼트를 나타내며, 얼라인먼트가 필요하지 않은 경우 1로 설정된다. 중첩 검사가 수행되면 플래그를 true로 설정하고 그렇지 않으면 false로 설정해야 합니다.

다음은 memmove 명령어를 사용하여 소스 주소가 가리키는 메모리 블록의 데이터를 대상 주소가 가리키는 메모리 블록으로 이동하는 예입니다.

%src = alloca [10 x i32], align 4
%dst = alloca [10 x i32], align 4
%size = getelementptr [10 x i32], [10 x i32]* %src, i32 0, i32 10
%sizeVal = ptrtoint i32* %size to i32
call void @llvm.memmove.p0i8.p0i8.i32(i8* bitcast ([10 x i32]* %dst to i8*), i8* bitcast ([10 x i32]* %src to i8*), i32 %sizeVal, i32 4, i1 false)

이 예제에서는 [10 x i32] 유형의 메모리 블록 두 개를 먼저 스택에 할당하고 메모리 블록의 크기를 getelementptr 명령어를 통해 가져옵니다. 그런 다음 memmove 명령이 호출되어 소스 주소가 가리키는 메모리 블록의 데이터를 대상 주소가 가리키는 메모리 블록으로 이동합니다. 소스 및 대상 주소를 i8* 유형 포인터로 변환하려면 비트캐스트 명령을 사용해야 합니다.

제어 흐름 지침

제어 흐름 지침에는 다음 지침이 포함됩니다.

  1. br: 조건 분기 명령으로, 조건에 따라 지정된 기본 블록으로 점프합니다.

  2. switch: 다방향 분기 명령, 입력값에 따라 다른 기본 블록으로 점프합니다.

  3. ret: 함수는 명령을 반환하고 함수가 호출된 위치로 돌아갑니다.

  4. indirectbr: 간접 분기 명령으로 지정된 번지에 저장된 기본 블록으로 점프합니다.

  5. invoke: 명령을 호출하고, 예외 처리 기능이 있는 함수를 호출하고, 예외가 발생하면 제어권을 넘깁니다.

  6. resume: Invoke 명령어 호출 시 발생한 예외를 복구하는 예외 복구 명령어.

  7. unreachable: 연결할 수 없는 명령으로, 프로그램을 이 지점까지 실행해서는 안 되며 이 지점까지 실행하면 정의되지 않은 동작이 발생함을 나타냅니다.

이러한 명령어는 LLVM IR에서 프로그램 흐름을 제어하는 ​​데 사용되며 제어 흐름 분석 및 SSA 형식을 기반으로 하는 변수 이름 변경과 같은 고급 최적화 기술을 가능하게 합니다. LLVM의 제어 흐름 명령어에는 조건부 분기 명령어 br, 다방향 분기 명령어 switch, 함수 반환 명령어 ret, 간접 분기 명령어 indirectbr, 호출 명령어 invoke, 예외 복구 명령어 resume및 도달 불가 명령어 가 포함됩니다 unreachable. 다음은 이러한 지침을 하나씩 설명하고 해당 코드 예제를 제공합니다.

1. 조건 분기 명령(br)

br명령은 조건에 따라 다른 기본 블록으로 점프하는 조건부 분기를 수행하는 데 사용됩니다. 구문은 다음과 같습니다.

br i1 <cond>, label <iftrue>, label <iffalse>

여기서 <cond>는 조건값으로 그 값이 참이면 <iftrue>로 표시된 기본 블록으로 이동하고 그렇지 않으면 로 <iffalse>표시된 기본 블록으로 이동합니다. 다음은 간단한 예입니다.

define i32 @test(i32 %a, i32 %b) {
    
    
  %cmp = icmp eq i32 %a, %b
  br i1 %cmp, label %equal, label %notequal

equal:
  ret i32 1

notequal:
  ret i32 0
}

test이 예제에서는 두 개의 정수 인수 %a%b. 먼저 icmp명령을 사용하여 두 값이 같은지 비교하고 결과를 %cmp. 그런 다음 br명령을 사용하여 %cmp의 값을 기준으로 다른 기본 블록으로 점프하고 같으면 반환하고 1, 그렇지 않으면 반환합니다 0.

2. 다방향 분기 명령(스위치)

switch명령은 입력 값에 따라 다른 기본 블록으로 점프하는 다중 분기를 수행하는 데 사용됩니다. 구문은 다음과 같습니다.

switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]

그 중 <intty>는 정수형이고, <value>는 입력값이며, <defaultdest>기본 점프의 기본 블록입니다. 각 후속 쌍은 <val>, <dest>옵션을 나타내며 같으면 <value>표시된 기본 블록 <val>으로 이동합니다 . <dest>다음은 예입니다.

define i32 @test(i32 %a) {
  switch i32 %a, label %default [
    i32 0, label %zero
    i32 1, label %one
  ]

zero:
  ret i32 0

one:
  ret i32 1

default:
  ret i32 -1
}

test이 예에서는 정수 매개변수를 사용하는 함수를 정의합니다 %a. 그런 다음 switch명령을 사용하여 %a의 값을 기준으로 다른 기본 블록으로 점프하고 %a같으면 0반환 하고 0같으면 반환 하고 그렇지 않으면 반환합니다 .%a11-1

3. 함수 반환 명령어(ret)

ret지시문은 함수에서 값을 반환하는 데 사용됩니다. 구문은 다음과 같습니다.

ret <type> <value>

그 중 는 <type>반환 값의 유형이며 는 <value>반환 값입니다. 함수가 값을 반환하지 않으면 반환 <type>해야 합니다 void. 다음은 예입니다.

define i32 @test(i32 %a, i32 %b) {
    
    
  %sum = add i32 %a, %b
  ret i32 %sum
}

test이 예제에서는 두 개의 정수 인수 %a%b. 먼저 add명령을 사용하여 이들을 함께 추가하고 결과를 에 저장합니다 %sum. ret그런 다음 명령에 의해 반환된 값을 사용합니다 %sum.

4. 간접 분기 명령(indirectbr)

indirectbr명령어는 간접 주소를 기반으로 다른 기본 블록으로 점프하는 데 사용됩니다. 구문은 다음과 같습니다.

indirectbr <type> <address>, [ label <dest1>, label <dest2>, ... ]

그 중 <type>점프 대상의 유형이며 <address>점프 대상의 주소에 대한 포인터입니다. 다음 각 <dest>토큰은 점프 대상 기본 블록을 나타냅니다. 다음은 예입니다.

define i32 @test(i32* %ptr) {
    
    
  %dest1 = label %one
  %dest2 = label %two
  indirectbr i8* %ptr, [ label %default, label %dest1, label %dest2 ]

one:
  ret i32 1

two:
  ret i32 2

default:
  ret i32 -1
}

test이 예제에서는 정수의 주소에 대한 포인터 인수를 취하는 함수를 정의합니다 %ptr. %one그런 다음 , 및 %two레이블이 지정된 세 개의 태그를 정의합니다 %default. indirectbr다음으로 명령을 사용하여 %ptr의 값을 기준으로 다른 기본 블록으로 점프하고 0같으면 반환 1하고 같으면 1반환 2하고 그렇지 않으면 반환합니다 -1.

5. 명령 호출(invoke)

invoke명령은 예외가 발생할 때 함수를 호출하고 제어를 전달하는 데 사용됩니다. 구문은 call명령과 비슷하지만 예외 처리 분기가 있습니다. 다음은 예입니다.

define void @test() {
    
    
  %catch = catchswitch within none [label %catch] unwind label %cleanup

  invoke void @foo()
          to label %normal
          unwind label %catch

normal:
  catchret from %catch to label %end

end:
  ret void

catch:
  %excp = catchpad within %catch [i8* null]
  call void @handle()
  catchret from %excp to label

test그 중 어떤 매개변수도 허용하지 않는 함수를 정의합니다 . 먼저 catchswitch지시문을 사용하여 예외 처리 블록을 만든 %catch다음 invoke지시문을 사용하여 함수를 호출합니다 foo. 함수 호출이 성공하면 태그로 이동하고 %normal그렇지 않으면 예외 처리 블록으로 이동합니다 %catch. 마커에서 지시문을 %normal사용하여 catchret제어를 예외 처리 블록으로 반환합니다 %catch. 예외 처리 블록에서는 디렉티브를 %catch사용하여 catchpad예외 처리 블록을 만들고 예외를 처리하는 함수를 %excp호출합니다 . 마지막으로 지시어를 handle사용하여 지시어의 태그 로 제어를 반환합니다 .catchretinvokeunwind%cleanup

6. 재개 명령(resume)

resume예외 처리 블록에서 실행을 재개하라는 명령. 구문은 다음과 같습니다.

resume <type> <value>

여기서 는 <type>복구할 예외 유형이고 <value>이상치 값입니다. 다음은 예입니다.

define void @test() {
    
    
  %catch = catchswitch within none [label %catch] unwind label %cleanup

  invoke void @foo()
          to label %normal
          unwind label %catch

normal:
  catchret from %catch to label %end

end:
  ret void

catch:
  %excp = catchpad within %catch [i8* null]
  %is_error = icmp eq i32 %excp, 1
  br i1 %is_error, label %handle, label %next

handle:
  call void @handle()
  resume void null

next:
  catchret from %excp to label %end
}

이 예제에서는 resume명령을 사용하여 예외 처리 블록에서 실행을 재개합니다. markup 에서 directive 를 사용하여 예외 처리 블록을 %catch만듭니다 . 그런 다음 명령을 사용하여 이상값을 와 비교 하고 비교에 따라 플래그 또는 플래그 로 이동합니다 . 마커에서 예외를 처리하는 함수를 호출 하고 명령을 사용하여 예외 처리 블록에서 실행을 재개합니다. marker 에서 지시어를 사용하여 지시어의 marker 로 제어를 반환합니다 .catchpad%excpicmp1%handle%next%handlehandleresume%nextcatchretinvokeunwind%cleanup

7. 도달할 수 없는 명령(unreachable)

unreachable명령은 프로그램이 여기에서 실행되지 않음을 나타내는 데 사용됩니다. 구문은 다음과 같습니다.

unreachable

다음은 예입니다.

define i32 @test(i32 %a, i32 %b) {
    
    
  %is_zero = icmp eq i32 %b, 0
  br i1 %is_zero, label %error, label %compute

compute:
  %result = sdiv i32 %a, %b
  ret i32 %result

error:
  unreachable
}

이 예제에서는 값을 test계산하는 함수를 정의합니다 . a / b마크업에서 지시어에 의해 계산된 값을 %compute사용 하고 지시어를 사용하여 결과를 반환합니다. 마커에서 명령을 사용하여 의 값 때문에 프로그램이 여기에서 실행되지 않음을 나타냅니다 . 이 경우 프로그램이 이미 충돌했기 때문에 아무 것도 반환할 필요가 없습니다.sdiva / bret%errorunreachableb0

기타 지침

위에서 소개한 7가지 제어 흐름 명령어 외에도 llvm에는 일반적으로 사용되는 다른 명령어가 있습니다. 이 기사에서는 phi, select, call, va_arg 및 landingpad의 다섯 가지 명령을 소개합니다.

1.파이

phi 명령은 기본 블록 간에 값을 전달하는 데 사용됩니다. 구문은 다음과 같습니다.

%result = phi <type> [ <value1>, <label1> ], [ <value2>, <label2> ], ...

여기서 <type>는 전달할 값의 유형이고 <value1>는 전달할 첫 번째 값 <label1>이며 첫 번째 값을 전달할 기본 블록입니다. 다른 <value>합계는 <label>비슷합니다. 다음은 예입니다.

define i32 @test(i32 %a, i32 %b) {
    
    
  %cmp = icmp slt i32 %a, %b
  br i1 %cmp, label %if_true, label %if_false

if_true:
  %result1 = add i32 %a, 1
  br label %merge

if_false:
  %result2 = add i32 %b, 1
  br label %merge

merge:
  %result = phi i32 [ %result1, %if_true ], [ %result2, %if_false ]
  ret i32 %result
}

이 예제에서는 합계 값을 test비교 하고 결과를 반환하는 함수를 정의합니다 . 마크에서는 명령에 의해 계산된 값을 사용하고 마크에서는 명령에 의해 계산된 값을 사용합니다 . 그런 다음 marker 에서 지시문을 사용하여 값을 선택합니다. 구체적으로 의 값이 이면 (즉 )의 값을 선택하고 그렇지 않으면 (즉 )의 값을 선택합니다 .ab%if_trueadda+1%if_falseaddb+1%mergephi%cmptrue%result1a+1%result2b+1

2. 선택

select 지시문은 조건에 따라 두 값 중 하나를 선택하는 데 사용됩니다. 구문은 다음과 같습니다.

%result = select i1 <cond>, <type> <iftrue>, <type> <iffalse>

여기서 <cond>는 테스트할 조건이고, <iftrue>는 조건이 참이면 반환할 값이고, <iffalse>는 조건이 거짓이면 반환할 값입니다. 다음은 예입니다.

define i32 @test(i32 %a, i32 %b) {
    
    
  %cmp = icmp slt i32 %a, %b
  %result = select i1 %cmp, i32 %a, i32 %b
  ret i32 %result
}

이 예제에서는 합계 값을 test비교 하고 결과를 반환하는 함수를 정의합니다 . 명령어를 사용하여 비교 하고 비교 결과를 에 저장합니다. 그런 다음 `에 기반한 지시문을 사용합니다.abicmpab%cmpselect

3.전화

호출 명령은 함수를 호출하는 데 사용됩니다. 구문은 다음과 같습니다.

%result = call <type> <function>(<argument list>)

여기서 <type>는 함수 반환 값의 유형이고, <function>는 호출할 함수의 이름이며, 는 <argument list>함수 매개변수 목록입니다. 다음은 예입니다.

declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b) {
    
    
  %sum = add i32 %a, %b
  %format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
  call i32 (i8*, ...) @printf(i8* %format_str, i32 %sum)
  ret i32 %sum
}

이 예제에서는 먼저 add지시문에 의해 계산된 a+b값을 사용한 다음 getelementptr지시문을 사용하여 @.str형식이 지정된 문자열을 포함하는 전역 문자열에 대한 포인터를 가져옵니다 %d\n. call마지막으로 지시어 를 사용하여 함수를 호출 하고 및 를 매개변수로 printf전달합니다 .%format_str%sum

4.va_arg

va_arg 지시문은 가변 함수에서 다음 인수를 가져오는 데 사용됩니다. 구문은 다음과 같습니다.

%result = va_arg <type*> <ap>

여기서 <type*>매개변수 유형에 대한 포인터이고 <ap>매개변수 목록을 포함하는 포인터입니다. 다음은 예입니다.

declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b, ...) {
    
    
  %ap = alloca i8*, i32 0
  store i8* %0, i8** %ap
  %sum = add i32 %a, %b
  %format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
  %next_arg = va_arg i32*, i8** %ap
  %value = load i32, i32* %next_arg
  %product = mul i32 %sum, %value
  call i32 (i8*, ...) @printf(i8* %format_str, i32 %product)
  ret i32 %sum
}

test이 예제에서는 두 개의 정수 인수 ab가변 개수의 다른 인수를 사용하는 가변 함수를 정의합니다 . 먼저 alloca명령을 사용하여 다음 매개변수의 주소를 저장하기 위해 스택에 포인터를 할당합니다. 그런 다음 명령을 사용하여 store해당 포인터에 첫 번째 인수의 주소를 저장합니다. 다음으로 명령어를 사용하여 va_arg다음 매개변수의 주소를 가져온 다음 load명령어를 사용하여 해당 매개변수의 값을 에 로드합니다 %value. mul마지막으로 명령어에 의해 계산된 값을 사용 (%a + %b) * %value하고 printf함수를 사용하여 표준 출력으로 출력합니다.

5.착륙장

landingpad 지시문은 예외 처리를 구현하는 데 사용됩니다. 구문은 다음과 같습니다.

%result = landingpad <type>

그 중 <type>예외 처리 함수의 반환 값 유형입니다. 다음은 예입니다.

declare void @llvm.landingpad(i8*, i32)
define void @test() personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
    
    
  %exn = landingpad i8*
  %exn_val = extractvalue {
    
     i8*, i32 } %exn, 0
  %exn_selector = extractvalue {
    
     i8*, i32 } %exn, 1
  call void @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str, i64 0, i64 0), i8* %exn_val, i32 %exn_selector)
  resume {
    
     i8*, i32 } %exn
}

이 예제에서는 예외 처리를 구현하는 함수를 정의합니다 test. 먼저 키워드를 사용하여 GCC C++ 예외 처리 라이브러리의 일부인 personality예외 처리 기능을 지정합니다 . __gxx_personality_v0그런 다음 지시문을 사용하여 landingpad예외 개체 및 예외 유형 번호를 가져옵니다. 지시문을 사용하여 extractvalue예외 개체와 예외 유형 번호로 분할하고 printf함수에 전달합니다. resume마지막으로 지시문을 사용하여 예외를 다시 발생시킵니다.

여기서 예외 처리 메커니즘은 상대적으로 복잡하고 더 깊은 이해가 필요하므로 여기서는 반복하지 않겠습니다.

참조

  1. LLVM 언어 참조 매뉴얼 — LLVM 17.0.0git 문서

추천

출처blog.csdn.net/HongHua_bai/article/details/129172305