radare2 icon indicating copy to clipboard operation
radare2 copied to clipboard

Handle tail call optimization

Open codido opened this issue 9 years ago • 36 comments

It appears function boundaries are a bit off when tail call optimization is used. For instance:

cat <<EOF > main.c
void foo(void);

int main()
{
    foo();
    return 0;
}
EOF

cat <<EOF > foo.c
void bar(void);

void foo()
{
    bar();
}
EOF

cat <<EOF > bar.c
void bar()
{
}
EOF

gcc -m32 -O2 main.c foo.c bar.c
r2 -A a.out -qc "pdf @sym.foo"

...yields:

╒ (fcn) sym.foo 16
│           ; CALL XREF from 0x08048301 (sym.foo)
│       ┌─< 0x08048410      e90b000000     jmp sym.bar
│       │   0x08048415      6690           nop
│       │   0x08048417      6690           nop
│       │   0x08048419      6690           nop
│       │   0x0804841b      6690           nop
│       │   0x0804841d      6690           nop
╘       │   0x0804841f      90             nop

codido avatar Mar 08 '16 02:03 codido

e anal.eobjmp=true

On 08 Mar 2016, at 03:30, Ido Yariv [email protected] wrote:

It appears function boundaries are a bit off when tail call optimization is used. For instance:

cat <<EOF > main.c void foo(void);

int main() { foo(); return 0; } EOF

cat <<EOF > foo.c void bar(void);

void foo() { bar(); } EOF

cat <<EOF > bar.c void bar() { } EOF

gcc -m32 -O2 main.c foo.c bar.c r2 -A a.out -qc "pdf @sym.foo" ...yields:

╒ (fcn) sym.foo 16 │ ; CALL XREF from 0x08048301 (sym.foo) │ ┌─< 0x08048410 e90b000000 jmp sym.bar │ │ 0x08048415 6690 nop │ │ 0x08048417 6690 nop │ │ 0x08048419 6690 nop │ │ 0x0804841b 6690 nop │ │ 0x0804841d 6690 nop ╘ │ 0x0804841f 90 nop — Reply to this email directly or view it on GitHub https://github.com/radare/radare2/issues/4258.

radare avatar Mar 08 '16 02:03 radare

This indeed seems to solve this, thanks! Any reason this shouldn't be true by default?

codido avatar Mar 08 '16 02:03 codido

Because this is not probably the right solution. Indeed you will see that functions will now be terminated as soon as you meet a jmp... I don't think anal.eobjmp really fixes the issue, because it creates many more problems.

ret2libc avatar Mar 08 '16 10:03 ret2libc

eobjmp doesnt exactly means that the funciton terminates on any jump. it must have some kind of conditions.. i know that this creates problems, because it is not easy to handle all sistuations in a generic way, did you spot any case where this is not working fine? we should have tests for this :3

On 08 Mar 2016, at 11:17, Riccardo Schirone [email protected] wrote:

Because this is not probably the right solution. Indeed you will see that functions will now be terminated as soon as you meet a jmp... I don't think anal.eobjmp really fixes the issue, because it creates many more problems.

— Reply to this email directly or view it on GitHub https://github.com/radare/radare2/issues/4258#issuecomment-193705822.

radare avatar Mar 08 '16 10:03 radare

Yes, a lot. This is /bin/cat for example:

╒ (fcn) entry0 60
│         0x100001540      55             push rbp
│         0x100001541      4889e5         mov rbp, rsp
│         0x100001544      4157           push r15
│         0x100001546      4156           push r14
│         0x100001548      4155           push r13
│         0x10000154a      4154           push r12
│         0x10000154c      53             push rbx
│         0x10000154d      50             push rax
│         0x10000154e      4889f3         mov rbx, rsi
│         0x100001551      4189fe         mov r14d, edi
│         0x100001554      488d35f50900.  lea rsi, section.4.__cstring
│         0x10000155b      bf02000000     mov edi, 2
│         0x100001560      e80b080000     call sym.imp.setlocale       ;[1]
│         0x100001565      4c8d3de50900.  lea r15, str.benstuv
│         0x10000156c      4c8d2d610100.  lea r13, 0x1000016d4
│         0x100001573      4c8b25b60a00.  mov r12, qword [reloc.__stdoutp_48]
╘    ┌──< 0x10000157a      eb14           jmp 0x100001590              ;[2]
   ┌────> 0x10000157c      c705ca0b0000.  mov dword [0x100002150], 1
   │ │    0x100001586      c705b00b0000.  mov dword [section.11.__common], 1
  ───└──> 0x100001590      4489f7         mov edi, r14d
   │      0x100001593      4889de         mov rsi, rbx

I also found out some time ago that there are BasicBlocks that do not belong to any function. As always, I think RAnal needs a lot of love!

ret2libc avatar Mar 09 '16 10:03 ret2libc

All yours if you are in the mood :D

On 09 Mar 2016, at 11:04, Riccardo Schirone [email protected] wrote:

Yes, a lot. This is /bin/cat for example:

╒ (fcn) entry0 60 │ 0x100001540 55 push rbp │ 0x100001541 4889e5 mov rbp, rsp │ 0x100001544 4157 push r15 │ 0x100001546 4156 push r14 │ 0x100001548 4155 push r13 │ 0x10000154a 4154 push r12 │ 0x10000154c 53 push rbx │ 0x10000154d 50 push rax │ 0x10000154e 4889f3 mov rbx, rsi │ 0x100001551 4189fe mov r14d, edi │ 0x100001554 488d35f50900. lea rsi, section.4.__cstring │ 0x10000155b bf02000000 mov edi, 2 │ 0x100001560 e80b080000 call sym.imp.setlocale ;[1] │ 0x100001565 4c8d3de50900. lea r15, str.benstuv │ 0x10000156c 4c8d2d610100. lea r13, 0x1000016d4 │ 0x100001573 4c8b25b60a00. mov r12, qword [reloc.__stdoutp_48] ╘ ┌──< 0x10000157a eb14 jmp 0x100001590 ;[2] ┌────> 0x10000157c c705ca0b0000. mov dword [0x100002150], 1 │ │ 0x100001586 c705b00b0000. mov dword [section.11.__common], 1 ───└──> 0x100001590 4489f7 mov edi, r14d │ 0x100001593 4889de mov rsi, rbx I also found out some time ago that there are BasicBlocks that do not belong to any function. As always, I think RAnal needs a lot of love!

— Reply to this email directly or view it on GitHub.

radare avatar Mar 09 '16 10:03 radare

Which condition can we assume when considering a tail continuation or not? Do you have more examples?

radare avatar Mar 13 '16 02:03 radare

For the PLT we can asume that a JMP is EOB when it points to a UJMP. can someone work a PR on this to see how much tests are broken and how many fixed?

radare avatar Mar 27 '16 14:03 radare

cc @rlaemmert

radare avatar May 04 '16 18:05 radare

I would assume that when there is a jump, the next block is always part of the current function (unless it is already part of another function). If during analysis you find out that one of those basic blocks is actually the start of another function, you could split the functions.

ret2libc avatar May 04 '16 18:05 ret2libc

What if theres data? Or just analize a single function?

Imho the approach would be to see if any branch in the current analyzed part theres a jump after that jmpback if not just end the function there

On 04 May 2016, at 20:45, Riccardo Schirone [email protected] wrote:

I would assume that when there is a jump, the next block is always part of the current function (unless it is already part of another function). If during analysis you find out that one of those basic blocks is actually the start of another function, you could split the functions.

— You are receiving this because you commented. Reply to this email directly or view it on GitHub

radare avatar May 07 '16 19:05 radare

If there is a direct jump to an address, there can't be data there (unless of course we are talking about obfuscated binaries, but then if we assume that, we may fail in thousands of ways). An address reachable through a direct jump is always part of the current function IMO. You can say there is a tail recursion optimisation only when, later, you find out that there is a call to one of the basic block or there is a function symbol there or you have other strong assumptions that make you think there is a function there.

For example:

  ┌──< 0x00000000      eb0a           jmp 0xc
  │    0x00000002      0000           add byte [rax], al
  │    0x00000004      0000           add byte [rax], al
  │    0x00000006      0000           add byte [rax], al
  │    0x00000008      0000           add byte [rax], al
  │    0x0000000a      0000           add byte [rax], al
  └──> 0x0000000c      55             push rbp
       0x0000000d      53             push rbx
       0x0000000e      b800000000     mov eax, 0
       0x00000013      5b             pop rbx
       0x00000014      5d             pop rbp
       0x00000015      c3             ret

In this case there is nothing, IMO, that can make you think that @ 0 there is a tail-recursion optimisation. So there should be just one function starting at 0, with two basic blocks (one starting at 0 and one starting at 0xc).

Instead, if we consider this code with a small change:

  ┌──< 0x00000000      eb0a           jmp 0xc
  │    0x00000002      0000           add byte [rax], al
  │    0x00000004      0000           add byte [rax], al
  │    0x00000006      0000           add byte [rax], al
  │    0x00000008      0000           add byte [rax], al
  │    0x0000000a      0000           add byte [rax], al
  └──> 0x0000000c      55             push rbp
       0x0000000d      53             push rbx
       0x0000000e      b800000000     mov eax, 0
       0x00000013      5b             pop rbx
       0x00000014      5d             pop rbp
       0x00000015      c3             ret
       0x00000016      0000           add byte [rax], al
       ;-- myfunction:
       0x00000018      55             push rbp
       0x00000019      e8eeffffff     call 0xc
       0x0000001e      5d             pop rbp
       0x0000001f      c3             ret

We have 3 functions: one at 0 with only one basic block that jumps outside the function, one at 0xc and another one at 0x18. When you start analysing the function @ 0, there isn't anything that tells you that the jump instruction is actually the result of a tail-call optimisation, so you have to consider the basic block @ 0xc as part of the function @ 0. Only later, when you analyse myfunction you find out that there is a call to a basic block that is not the entry point of the relative function and so, you can split the function @ 0 in two functions, one starting at 0 and one starting at 0xc.

This is how I see this problem.

About the analysis of only one function... well, if you want to analyse only part of the binary you can't expect to gather every aspect of the binary :P you have to live with the fact that you'll have part of all the info.

ret2libc avatar May 08 '16 21:05 ret2libc

Many nonreturn calls are constructed with jumps nowadays, so you will end up having many imports as bb of other functions

On 08 May 2016, at 23:58, Riccardo Schirone [email protected] wrote:

If there is a direct jump to an address, there can't be data there (unless of course we are talking about obfuscated binaries, but then if we assume that, we may fail in thousands of ways). An address reachable through a direct jump is always part of the current function IMO. You can say there is a tail recursion optimisation only when, later, you find out that there is a call to one of the basic block or there is a function symbol there or you have other strong assumptions that make you think there is a function there.

For example:

┌──< 0x00000000 eb0a jmp 0xc │ 0x00000002 0000 add byte [rax], al │ 0x00000004 0000 add byte [rax], al │ 0x00000006 0000 add byte [rax], al │ 0x00000008 0000 add byte [rax], al │ 0x0000000a 0000 add byte [rax], al └──> 0x0000000c 55 push rbp 0x0000000d 53 push rbx 0x0000000e b800000000 mov eax, 0 0x00000013 5b pop rbx 0x00000014 5d pop rbp 0x00000015 c3 ret In this case there is nothing, IMO, that can make you think that @ 0 there is a tail-recursion optimisation. So there should be just one function starting at 0, with two basic blocks (one starting at 0 and one starting at 0xc).

Instead, if we consider this code with a small change:

┌──< 0x00000000 eb0a jmp 0xc │ 0x00000002 0000 add byte [rax], al │ 0x00000004 0000 add byte [rax], al │ 0x00000006 0000 add byte [rax], al │ 0x00000008 0000 add byte [rax], al │ 0x0000000a 0000 add byte [rax], al └──> 0x0000000c 55 push rbp 0x0000000d 53 push rbx 0x0000000e b800000000 mov eax, 0 0x00000013 5b pop rbx 0x00000014 5d pop rbp 0x00000015 c3 ret 0x00000016 0000 add byte [rax], al ;-- myfunction: 0x00000018 55 push rbp 0x00000019 e8eeffffff call 0xc 0x0000001e 5d pop rbp 0x0000001f c3 ret We have 3 functions: one at 0 with only one basic block that jumps outside the function, one at 0xc and another one at 0x18. When you start analysing the function @ 0, there isn't anything that tells you that the jump instruction is actually the result of a tail-call optimisation, so you have to consider the basic block @ 0xc as part of the function @ 0. Only later, when you analyse myfunction you find out that there is a call to a basic block that is not the entry point of the relative function and so, you can split the function @ 0 in two functions, one starting at 0 and one starting at 0xc.

This is how I see this problem.

About the analysis of only one function... well, if you want to analyse only part of the binary you can't expect to gather every aspect of the binary :P you have to live with the fact that you'll have part of all the info.

— You are receiving this because you commented. Reply to this email directly or view it on GitHub

radare avatar May 08 '16 22:05 radare

And we cannot relay always on the sections

On 08 May 2016, at 23:58, Riccardo Schirone [email protected] wrote:

If there is a direct jump to an address, there can't be data there (unless of course we are talking about obfuscated binaries, but then if we assume that, we may fail in thousands of ways). An address reachable through a direct jump is always part of the current function IMO. You can say there is a tail recursion optimisation only when, later, you find out that there is a call to one of the basic block or there is a function symbol there or you have other strong assumptions that make you think there is a function there.

For example:

┌──< 0x00000000 eb0a jmp 0xc │ 0x00000002 0000 add byte [rax], al │ 0x00000004 0000 add byte [rax], al │ 0x00000006 0000 add byte [rax], al │ 0x00000008 0000 add byte [rax], al │ 0x0000000a 0000 add byte [rax], al └──> 0x0000000c 55 push rbp 0x0000000d 53 push rbx 0x0000000e b800000000 mov eax, 0 0x00000013 5b pop rbx 0x00000014 5d pop rbp 0x00000015 c3 ret In this case there is nothing, IMO, that can make you think that @ 0 there is a tail-recursion optimisation. So there should be just one function starting at 0, with two basic blocks (one starting at 0 and one starting at 0xc).

Instead, if we consider this code with a small change:

┌──< 0x00000000 eb0a jmp 0xc │ 0x00000002 0000 add byte [rax], al │ 0x00000004 0000 add byte [rax], al │ 0x00000006 0000 add byte [rax], al │ 0x00000008 0000 add byte [rax], al │ 0x0000000a 0000 add byte [rax], al └──> 0x0000000c 55 push rbp 0x0000000d 53 push rbx 0x0000000e b800000000 mov eax, 0 0x00000013 5b pop rbx 0x00000014 5d pop rbp 0x00000015 c3 ret 0x00000016 0000 add byte [rax], al ;-- myfunction: 0x00000018 55 push rbp 0x00000019 e8eeffffff call 0xc 0x0000001e 5d pop rbp 0x0000001f c3 ret We have 3 functions: one at 0 with only one basic block that jumps outside the function, one at 0xc and another one at 0x18. When you start analysing the function @ 0, there isn't anything that tells you that the jump instruction is actually the result of a tail-call optimisation, so you have to consider the basic block @ 0xc as part of the function @ 0. Only later, when you analyse myfunction you find out that there is a call to a basic block that is not the entry point of the relative function and so, you can split the function @ 0 in two functions, one starting at 0 and one starting at 0xc.

This is how I see this problem.

About the analysis of only one function... well, if you want to analyse only part of the binary you can't expect to gather every aspect of the binary :P you have to live with the fact that you'll have part of all the info.

— You are receiving this because you commented. Reply to this email directly or view it on GitHub

radare avatar May 08 '16 22:05 radare

Why? If you have symbols you can also use them to know when another function starts.. If you don't have them, then you can special handling the plt stubs. If you are really in worst case, at most you will consider some more bb as part of that function...

Btw, my algorithm doesn't relay on section info

ret2libc avatar May 09 '16 06:05 ret2libc

you cant relay on symbols always either

On 09 May 2016, at 08:40, Riccardo Schirone [email protected] wrote:

Why? If you have symbols you can also use them to know when another function starts.. If you don't have them, then you can special handling the plt stubs. If you are really in worst case, at most you will consider some more bb as part of that function...

Btw, my algorithm doesn't relay on section info

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/radare/radare2/issues/4258#issuecomment-217786997

radare avatar May 09 '16 09:05 radare

I know. If you don't have symbols you can detect plt stubs. If neither that is true, well, you are jumping simply somewhere else and there is nothing that can make you say that you are jumping to a different function :)

ret2libc avatar May 09 '16 11:05 ret2libc

we dont supported functions with shared basic blocks.. so this approach will fuckup the whole thing

On 09 May 2016, at 13:03, Riccardo Schirone [email protected] wrote:

I know. If you don't have symbols you can detect plt stubs. If neither that is true, well, you are jumping simply somewhere else and there is nothing that can make you say that you are jumping to a different function :)

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/radare/radare2/issues/4258#issuecomment-217836109

radare avatar May 09 '16 14:05 radare

Yeah there are a lot of things where analysis is not good enough :) It's in the design of the analysis itself. So, just because the analysis currently sucks, it doesn't mean we should continue adding wrong algorithms on top of that

ret2libc avatar May 09 '16 16:05 ret2libc

Feel free to read a2f or write your own analysis loop, ehich can be done in a plugin too

On 09 May 2016, at 18:01, Riccardo Schirone [email protected] wrote:

Yeah there are a lot of things where analysis is not good enough :) It's in the design of the analysis itself. So, just because the analysis currently sucks, it doesn't mean we should continue adding wrong algorithms on top of that

— You are receiving this because you commented. Reply to this email directly or view it on GitHub

radare avatar May 09 '16 23:05 radare

can we close this issue?

radare avatar Oct 08 '16 01:10 radare

@radare Sorry for the delayed response. Running the original example still seems a tad weird:

╒ (fcn) sym.foo 7
│   sym.foo ();
│           ; CALL XREF from 0x08048301 (sym.main)
│       ┌─< 0x08048410      e90b000000     jmp sym.bar
        │   0x08048415      6690           nop

codido avatar Oct 13 '16 14:10 codido

Moving to 1.2, dont wanna touch the anal much more for next week release. Focusing on stability now.

radare avatar Dec 12 '16 17:12 radare

Off-topic. DWARF 5 defines DW_AT_call_tail_call. There are also GNU extensions for tail calls.

Off-topic. many ideas from DWARF could be incorporated.

MaskRay avatar Sep 04 '17 01:09 MaskRay

having some binaries and tests to check this will be good

radare avatar Dec 18 '17 16:12 radare

@all please provide a test binaries @sivaramaaa please check this case for detecting variables/arguments.

XVilka avatar Jul 19 '18 08:07 XVilka

Hi, It seems that anal.eobjmp or anal.jmp.eobj is disabled in the new version. Is there any other methods to detect tail call optimization now in radare2?

bin2415 avatar Mar 20 '20 14:03 bin2415

tail call optimizations should work fine by default now, if theres any regression please fill an issue and i'll have a look

On 20 Mar 2020, at 15:15, bin2415 [email protected] wrote:

Hi, It seems that anal.eobjmp or anal.jmp.eobj is disabled on the new version. Is there any other methods to detect tail call optimization now in radare2?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/radareorg/radare2/issues/4258#issuecomment-601722751, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG75FXGIAEQDB2ZVE7QRP3RIN3BJANCNFSM4B5NRVWQ.

radare avatar Mar 20 '20 15:03 radare

Great!
Recently, I am learning the algorithm of detecting tail call optimization, could you please give any hint about where is the source code to handle tail call optimization?

Thanks!

bin2415 avatar Mar 20 '20 15:03 bin2415

ther'es no formal definition for this and its a bit blurry by definition because it can be confused with obfuscated code so it ends up being a bit difficult to solve in a generic way, you can use metrics like lastr intruction of a function is a jump, but it can be a conditional jump too. and last instruction of a function can be inside the function because there can be more than one tailcall in the same function and this can be a jump forward or backward, so you can use some boundaries to determine this but it is always a bit fuzzy to solve.

the code is in libr/anal/fcn.c

On 20 Mar 2020, at 16:19, bin2415 [email protected] wrote:

Great! Recently, I am learning the algorithm of detecting tail call optimization, could you please give any hint about where is the source code to handle tail call optimization?

Thanks!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/radareorg/radare2/issues/4258#issuecomment-601754092, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG75FVDIAU54OPLCCR3453RIOCOZANCNFSM4B5NRVWQ.

radare avatar Mar 22 '20 12:03 radare

Great! Thanks! :)

bin2415 avatar Mar 22 '20 13:03 bin2415

Hi, is this the logic that detecting tail call optimization? If so, could you please give any advice to set the anal.jmp.tailcall value?

bin2415 avatar Mar 22 '20 15:03 bin2415

Screenshot 2020-03-22 at 17 57 02

radare avatar Mar 22 '20 17:03 radare

Yeah, Thanks! A quick question, is there any advice to set the anal.jmp.tailcall value?

bin2415 avatar Mar 22 '20 17:03 bin2415

@trufae @ret2libc @bin2415 should we close this issue? Do we have tests for this feature?

XVilka avatar Sep 08 '20 05:09 XVilka

We have some tests afaik, but there are a bunch of problems to solve for proper tailcall support. Right now it depends if the tailing function is being analyzed before and also if the distance is bigger than a specific value that is configurable by an eval var. but all those are just random assumptions that must be addressed properly

trufae avatar Sep 08 '20 16:09 trufae