IL 분석을 통해 얻은 것
C# 의 IL 을 분석 할 수 있는 능력을 위해 IL코드를 직접 분석하는 도중 발생한 사소한 이슈에 대해 제대로 알고 기억하기 위해 이 글을 작성함
IL 분석
Perplexity AI 를 통해 IL 코드를 무작정 해석해 달라 하고, 각 명령어가 뭘 의미하는지 부터 알아보게 되었다
public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
{
return value == null || value.Length == 0;
}
.method public hidebysig static bool
IsNullOrEmpty(
string 'value'
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(unsigned int8)
= (01 00 02 00 00 ) // .....
// unsigned int8(2) // 0x02
.param [1]
.custom instance void System.Diagnostics.CodeAnalysis.NotNullWhenAttribute::.ctor(bool)
= (01 00 00 00 00 ) // .....
// bool(false)
.maxstack 8
// [505 13 - 505 55]
IL_0000: ldarg.0 // 'value'
IL_0001: brfalse.s IL_000d
IL_0003: ldarg.0 // 'value'
IL_0004: callvirt instance int32 System.String::get_Length()
IL_0009: ldc.i4.0
IL_000a: ceq
IL_000c: ret
IL_000d: ldc.i4.1
IL_000e: ret
} // end of method String::IsNullOrEmpty
가장 먼저 본 것은 String.IsNullOrEmpty()의 IL 코드이다
그 중에서도 IL_0000 으로 시작하는 asm 같은 부분을 자세히 보게 되었다
// [505 13 - 505 55]
IL_0000: ldarg.0 // 'value'
IL_0001: brfalse.s IL_000d
IL_0003: ldarg.0 // 'value'
IL_0004: callvirt instance int32 System.String::get_Length()
IL_0009: ldc.i4.0
IL_000a: ceq
IL_000c: ret
IL_000d: ldc.i4.1
IL_000e: ret
IL_0000: ldarg.0- 매개변수를 스택에 저장함
- 여기서는
String.IsNullOrEmtpy(string value)중value에 해당된다
IL_0001: brfalse.s IL_000d- 0/null 인 경우
IL_000d로 분기함 - 여기서는
value가 false 인 경우 분기하도록 한다
- 0/null 인 경우
IL0003: ldarg.0,IL0004: callvirt instance int32 System.String::get_Length()- 매개변수를 다시 스택에 저장, value에 대한 Length를 호출
IL_0009: ldc.i4.0- 0을 스택에 저장
IL_000a: ceq- 같은지 비교 -> 0 또는 1을 스택에 저장
- 여기서는 value의 Length와 스택에 저장된 0을 비교한다
IL_000c: ret- return
- 비교 결과를 return한다
IL_000d: ldc.i4.1- 1을 스택에 저장
IL_0001을 통해 분기 한 결과로 value가 null 일 경우 1을 반환하도록 하기 위한 명령어다
IL_000e: ret- return
IL_000d를 통해 저장된 1을 return한다
인스턴스 메서드의 ldarg 인덱스
ConnectAsync IL
- 다음은 아무 코드나 읽어보자 해서
Socket.Tasks.cs에 존재하는ConnectAsync에 대한 IL 코드를 읽어보게 되었다
public ValueTask ConnectAsync(string host, int port, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(host);
EndPoint ep = IPAddress.TryParse(host, out IPAddress? parsedAddress) ? (EndPoint)
new IPEndPoint(parsedAddress, port) :
new DnsEndPoint(host, port);
return ConnectAsync(ep, cancellationToken);
}
method public hidebysig instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask
ConnectAsync(
string host,
int32 port,
valuetype [System.Runtime]System.Threading.CancellationToken cancellationToken
) cil managed
{
.maxstack 3
.locals (
[0] class [System.Net.Primitives]System.Net.EndPoint ep,
[1] class [System.Net.Primitives]System.Net.IPAddress parsedAddress
)
// [214 13 - 214 53]
IL_0000: ldarg.1 // host
IL_0001: ldstr "host"
IL_0006: call void [System.Runtime]System.ArgumentNullException::ThrowIfNull(object, string)
// [216 13 - 218 45]
IL_000b: ldarg.1 // host
IL_000c: ldloca.s parsedAddress
IL_000e: call bool [System.Net.Primitives]System.Net.IPAddress::TryParse(string, class [System.Net.Primitives]System.Net.IPAddress&)
IL_0013: brtrue.s IL_001e
IL_0015: ldarg.1 // host
IL_0016: ldarg.2 // port
IL_0017: newobj instance void [System.Net.Primitives]System.Net.DnsEndPoint::.ctor(string, int32)
IL_001c: br.s IL_0025
IL_001e: ldloc.1 // parsedAddress
IL_001f: ldarg.2 // port
IL_0020: newobj instance void [System.Net.Primitives]System.Net.IPEndPoint::.ctor(class [System.Net.Primitives]System.Net.IPAddress, int32)
IL_0025: stloc.0 // ep
// [219 13 - 219 56]
IL_0026: ldarg.0 // this
IL_0027: ldloc.0 // ep
IL_0028: ldarg.3 // cancellationToken
IL_0029: call instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask System.Net.Sockets.Socket::ConnectAsync(class [System.Net.Primitives]System.Net.EndPoint, valuetype [System.Runtime]System.Threading.CancellationToken)
IL_002e: ret
} // end of method Socket::ConnectAsync
- 매우 길지만
ldarg.0과ldarg.1을 설명하기 위한 IL 코드이다 - 앞서 등장했던
ldarg와brtrue.s등 무엇을 하려는지는 이해를 할 수 있었다
궁금했던 점
- 처음에는 당연히
ldarg.0이 host를 의미할 것이라고 생각했다. 그러나 IL코드는ldarg.1이 host를 의미하는 것으로 되어있었고, 매개변수의 인덱싱이 zero-based가 아닌가 생각도 했었다. - AI를 통해 질문을 해 보았다.
- “
ldarg.1이 host인데ldarg.0은 무엇인가? 설마 제로베이스가 아닌건가?”
- “
- 답은 간단했다
- “인스턴스 메서드는 항상
this를 포함하여ldarg.0은 this이다.”
- “인스턴스 메서드는 항상
- IL에도 당연한 듯이 적혀 있었다 그러나 의미를 몰랐다
IL_0026: ldarg.0 //this
결론
인스턴스 메서드
class MyClass
{
public void Method()
{
// ...
}
}
- 클래스의 인스턴스를 통해 호출되는 메서드
- 항상
this를 매개변수로 포함한다- python의
def method(self, x): ...처럼 내부적으로 self가 자동으로 들어가는 구조와 비슷하다
- python의
- 특정 인스턴스를 통해 호출하는 인스턴스 매개변수는
ldarg.0은 항상this이고, 이후로 해당 메서드의 매개변수가 차례대로ldarg.1,ldarg.2, … 로 매핑된다
정적 메서드
class MyClass
{
public static void Method()
{
// ...
}
}
- 클래스 자체에 붙어있는 메서드
- 인스턴스가 존재하지 않음
ldarg.0에 메서드의 첫 번째 인자부터 매핑된다
느낀점
- 어찌보면 당연한 얘기인데 C# 지식에 대해서 다시 생각하게되는 계기가 된 것 같다
- IL 코드가 asm 처럼 무작정 어려울 것 같아 시도를 하지 못했었는데 생각보다 읽기 편하고 재미있어서 다행이다
- 이후에는 IL 을 통한 최적화 등 못해본 것들도 도전해 봐야겠다는 생각을 하게 되었다
Comments