@@ -2,6 +2,7 @@ package resurgo_test
22
33import (
44 "bytes"
5+ "debug/elf"
56 "os"
67 "os/exec"
78 "path/filepath"
@@ -417,108 +418,6 @@ func TestDetectCallSitesFromELF_InvalidReader(t *testing.T) {
417418 }
418419}
419420
420- func TestDetectFunctions (t * testing.T ) {
421- // Test combined prologue + call site detection
422- // Create code with:
423- // 1. A function with prologue that is also called
424- // 2. A function with prologue but not called
425- // 3. A called address without prologue
426-
427- // AMD64 code:
428- // 0x00: push rbp (0x55)
429- // 0x01: mov rbp, rsp (0x48 0x89 0xe5)
430- // 0x04: call 0x20 (0xE8 0x17 0x00 0x00 0x00) - calls function at 0x20
431- // 0x09: ret (0xC3)
432- // 0x0A: padding
433- // ...
434- // 0x20: push rbp (0x55) - function with prologue, called from 0x04
435- // 0x21: mov rbp, rsp (0x48 0x89 0xe5)
436- // 0x24: ret (0xC3)
437- // 0x25: padding
438- // ...
439- // 0x30: push rbx (0x53) - function with prologue, not called
440- // 0x31: ret (0xC3)
441- // 0x32: padding
442- // ...
443- // 0x40: ret (0xC3) - called target without prologue
444-
445- code := make ([]byte , 0x50 )
446- // Function at 0x00 with prologue
447- code [0x00 ] = 0x55 // push rbp
448- code [0x01 ] = 0x48 // mov rbp, rsp
449- code [0x02 ] = 0x89
450- code [0x03 ] = 0xe5
451- // Call to 0x20 (rel32 = 0x20 - (0x04 + 5) = 0x17)
452- code [0x04 ] = 0xE8 // call
453- code [0x05 ] = 0x17
454- code [0x06 ] = 0x00
455- code [0x07 ] = 0x00
456- code [0x08 ] = 0x00
457- code [0x09 ] = 0xC3 // ret
458-
459- // Function at 0x20 with prologue, called
460- code [0x20 ] = 0x55 // push rbp
461- code [0x21 ] = 0x48 // mov rbp, rsp
462- code [0x22 ] = 0x89
463- code [0x23 ] = 0xe5
464- code [0x24 ] = 0xC3 // ret
465-
466- // Add RET before function at 0x30 to establish function boundary
467- code [0x2F ] = 0xC3 // ret
468-
469- // Function at 0x30 with prologue, not called
470- code [0x30 ] = 0x53 // push rbx (callee-saved)
471- code [0x31 ] = 0xC3 // ret
472-
473- // No code at 0x40 (would be called target without prologue in a real scenario)
474-
475- candidates , err := resurgo .DetectFunctions (code , 0 , resurgo .ArchAMD64 )
476- if err != nil {
477- t .Fatalf ("unexpected error: %v" , err )
478- }
479-
480- // Should find at least 3 candidates:
481- // - 0x00: prologue-only (main function, might not be called internally)
482- // - 0x20: both (prologue + called)
483- // - 0x30: prologue-only (not called)
484-
485- if len (candidates ) < 3 {
486- t .Fatalf ("expected at least 3 candidates, got %d: %+v" , len (candidates ), candidates )
487- }
488-
489- // Find candidate at 0x20 (should have both prologue and call target)
490- var candidate0x20 * resurgo.FunctionCandidate
491- for i := range candidates {
492- if candidates [i ].Address == 0x20 {
493- candidate0x20 = & candidates [i ]
494- break
495- }
496- }
497-
498- if candidate0x20 == nil {
499- t .Fatal ("expected candidate at address 0x20, got none" )
500- }
501-
502- if candidate0x20 .DetectionType != resurgo .DetectionPrologueCallSite {
503- t .Errorf ("expected detection type 'both', got %s" , candidate0x20 .DetectionType )
504- }
505-
506- if candidate0x20 .Confidence != resurgo .ConfidenceHigh {
507- t .Errorf ("expected high confidence, got %s" , candidate0x20 .Confidence )
508- }
509-
510- if len (candidate0x20 .CalledFrom ) == 0 {
511- t .Error ("expected at least one caller, got none" )
512- }
513-
514- t .Logf ("Found %d function candidates" , len (candidates ))
515- for _ , c := range candidates {
516- t .Logf (" 0x%x: %s (confidence: %s, called from: %d, jumped from: %d)" ,
517- c .Address , c .DetectionType , c .Confidence ,
518- len (c .CalledFrom ), len (c .JumpedFrom ))
519- }
520- }
521-
522421func TestDetectCallSitesAMD64_ENDBR (t * testing.T ) {
523422 // ENDBR64 (f3 0f 1e fa) followed by a call should detect the call,
524423 // skipping the ENDBR64 instruction transparently.
@@ -582,59 +481,36 @@ func TestDetectCallSites_EmptyInput(t *testing.T) {
582481 }
583482}
584483
585- func TestDetectFunctions_UnsupportedArch (t * testing.T ) {
586- _ , err := resurgo .DetectFunctions ([]byte {0x00 }, 0 , resurgo .Arch ("mips" ))
587- if err == nil {
588- t .Fatal ("expected error for unsupported architecture, got nil" )
589- }
590- }
591-
592- func TestDetectFunctions_JumpTarget (t * testing.T ) {
593- // Verify that unconditional JMPs create jump-target candidates
594- // (fix #2: medium-confidence edges now pass the filter).
595- //
596- // AMD64 code:
597- // 0x00: jmp 0x10 (E9 0B 00 00 00) - jump to 0x10
598- // 0x05: nop padding...
599- // 0x10: ret (C3) - jump target without prologue
600484
485+ func TestDetectCallSites_JumpTarget (t * testing.T ) {
486+ // Verify that unconditional JMPs create jump-target candidates.
487+ // jmp 0x10: E9 0B 00 00 00 at 0x00, target = 0x10
601488 code := make ([]byte , 0x20 )
602- // jmp to 0x10: rel32 = 0x10 - (0x00 + 5) = 0x0B
603489 code [0x00 ] = 0xE9
604490 code [0x01 ] = 0x0B
605491 code [0x02 ] = 0x00
606492 code [0x03 ] = 0x00
607493 code [0x04 ] = 0x00
608494 code [0x10 ] = 0xC3 // ret
609495
610- candidates , err := resurgo .DetectFunctions (code , 0 , resurgo .ArchAMD64 )
496+ edges , err := resurgo .DetectCallSites (code , 0 , resurgo .ArchAMD64 )
611497 if err != nil {
612498 t .Fatalf ("unexpected error: %v" , err )
613499 }
614500
615- // Look for a jump-target candidate at 0x10
616- var found * resurgo.FunctionCandidate
617- for i := range candidates {
618- if candidates [i ].Address == 0x10 {
619- found = & candidates [i ]
501+ var found * resurgo.CallSiteEdge
502+ for i := range edges {
503+ if edges [i ].TargetAddr == 0x10 {
504+ found = & edges [i ]
620505 break
621506 }
622507 }
623-
624508 if found == nil {
625- t .Fatal ("expected candidate at address 0x10, got none" )
626- }
627-
628- if found .DetectionType != resurgo .DetectionJumpTarget {
629- t .Errorf ("expected detection type 'jump-target', got %s" , found .DetectionType )
630- }
631-
632- if len (found .JumpedFrom ) == 0 {
633- t .Error ("expected at least one JumpedFrom entry, got none" )
509+ t .Fatal ("expected jump edge to 0x10, got none" )
634510 }
635511}
636512
637- func TestDetectFunctionsFromELF (t * testing.T ) {
513+ func TestDetectFunctions (t * testing.T ) {
638514 binPath := filepath .Join (t .TempDir (), "demo-app" )
639515 args := []string {"build" , "-o" , binPath , "testdata/demo-app.go" }
640516
@@ -644,13 +520,13 @@ func TestDetectFunctionsFromELF(t *testing.T) {
644520 t .Fatalf ("failed to compile demo-app: %v\n %s" , err , out )
645521 }
646522
647- f , err := os .Open (binPath )
523+ f , err := elf .Open (binPath )
648524 if err != nil {
649- t .Fatalf ("failed to open compiled binary: %v" , err )
525+ t .Fatalf ("failed to open ELF binary: %v" , err )
650526 }
651527 defer f .Close ()
652528
653- candidates , err := resurgo .DetectFunctionsFromELF (f )
529+ candidates , err := resurgo .DetectFunctions (f )
654530 if err != nil {
655531 t .Fatalf ("unexpected error: %v" , err )
656532 }
@@ -659,24 +535,75 @@ func TestDetectFunctionsFromELF(t *testing.T) {
659535 t .Fatal ("expected at least one function candidate, got none" )
660536 }
661537
662- // Count by detection type
663538 counts := make (map [resurgo.DetectionType ]int )
664539 for _ , c := range candidates {
665540 counts [c .DetectionType ]++
666541 }
667542 t .Logf ("total candidates: %d, by type: %v" , len (candidates ), counts )
668543
669- // Should have both prologue-only and call-target candidates at minimum
670544 if counts [resurgo .DetectionPrologueOnly ] == 0 {
671545 t .Error ("expected at least one prologue-only candidate" )
672546 }
673547}
674548
675- func TestDetectFunctionsFromELF_InvalidReader (t * testing.T ) {
549+ // TestDisasmDetector verifies that DisasmDetector, when run against a real ELF
550+ // binary, produces candidates with the expected detection types and that
551+ // functions both called and matching a prologue pattern are promoted to
552+ // DetectionPrologueCallSite with ConfidenceHigh.
553+ func TestDisasmDetector (t * testing.T ) {
554+ binPath := filepath .Join (t .TempDir (), "demo-app" )
555+ cmd := exec .Command ("go" , "build" , "-o" , binPath , "testdata/demo-app.go" )
556+ cmd .Env = append (os .Environ (), "CGO_ENABLED=0" , "GOARCH=amd64" )
557+ if out , err := cmd .CombinedOutput (); err != nil {
558+ t .Fatalf ("failed to compile demo-app: %v\n %s" , err , out )
559+ }
560+
561+ f , err := elf .Open (binPath )
562+ if err != nil {
563+ t .Fatalf ("failed to open ELF binary: %v" , err )
564+ }
565+ defer f .Close ()
566+
567+ candidates , err := resurgo .DisasmDetector (f )
568+ if err != nil {
569+ t .Fatalf ("DisasmDetector: %v" , err )
570+ }
571+
572+ if len (candidates ) == 0 {
573+ t .Fatal ("expected at least one candidate, got none" )
574+ }
575+
576+ counts := make (map [resurgo.DetectionType ]int )
577+ for _ , c := range candidates {
578+ counts [c .DetectionType ]++
579+ }
580+ t .Logf ("total candidates: %d, by type: %v" , len (candidates ), counts )
581+
582+ // Disasm must find functions via prologue pattern.
583+ if counts [resurgo .DetectionPrologueOnly ] == 0 && counts [resurgo .DetectionPrologueCallSite ] == 0 {
584+ t .Error ("expected prologue-based candidates, got none" )
585+ }
586+
587+ // Functions confirmed by both prologue and call-site must be ConfidenceHigh
588+ // and must carry at least one caller address.
589+ for _ , c := range candidates {
590+ if c .DetectionType == resurgo .DetectionPrologueCallSite {
591+ if c .Confidence != resurgo .ConfidenceHigh {
592+ t .Errorf ("0x%x: expected ConfidenceHigh for prologue-callsite, got %s" , c .Address , c .Confidence )
593+ }
594+ if len (c .CalledFrom ) == 0 && len (c .JumpedFrom ) == 0 {
595+ t .Errorf ("0x%x: prologue-callsite candidate has no caller or jump source" , c .Address )
596+ }
597+ }
598+ }
599+ }
600+
601+ func TestDetectFunctions_InvalidELF (t * testing.T ) {
676602 r := bytes .NewReader ([]byte {0x00 , 0x01 , 0x02 , 0x03 })
677- _ , err := resurgo . DetectFunctionsFromELF (r )
603+ f , err := elf . NewFile (r )
678604 if err == nil {
679- t .Fatal ("expected error for invalid ELF data, got nil" )
605+ f .Close ()
606+ t .Fatal ("expected elf.NewFile to fail on invalid data" )
680607 }
681608}
682609
0 commit comments