unexpected jump

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

unexpected jump

#1 Post by Liviu » 25 Jan 2012 19:03

This may well be an xp.sp3 quirk, haven't tried it elsewhere, yet. But it's quite odd. Here's the code...

Code: Select all

@echo off
setlocal disableDelayedExpansion
echo.
echo * %~n0 { %1 }

set /a rearg1 = %1 - 1
if %rearg1% leq 0 goto :done

echo * before call :exec %0 %rearg1%
call :exec-fail %%0 rearg1
echo ..after call :exec %0 %rearg1%

:done
echo ..%~n0 %1 done
endlocal
goto :eof

:exec
call echo * begin :exec %%1 %%%~2%%
setlocal enableDelayedExpansion
set "exec=%1 !%~2!"
echo * before !exec!
!exec!
echo ..after %1 !%~2!
endlocal
echo * end :exec %1 !%~2!
goto :eof

:exec-fail
call echo * begin :exec2 %%1 %%%~2%%
setlocal enableDelayedExpansion
echo * before %1 !%~2!
%1 !%~2!
echo ..after %1 !%~2!
endlocal
echo * end :exec2 %1 !%~2!
goto :eof

Save it as, say, recurse.cmd, and run it with an argument of 2. Output comes out as expected, copied below.

Code: Select all

C:\>recurse 2

* recurse { 2 }
* before call :exec recurse 1
* begin :exec recurse 1
* before recurse 1

* recurse { 1 }
..recurse 1 done
..after call :exec recurse 1
..recurse 2 done

Now, replace the "call :exec %%0 rearg1" line towards the top with "call :exec-fail %%0 rearg1" and run again...

Code: Select all

C:\>recurse 2

* recurse { 2 }
* before call :exec recurse 1
* begin :exec2 recurse 1
* before recurse 1
* begin :exec2 !rearg1! %
* before 1
'1' is not recognized as an internal or external command,
operable program or batch file.
..after 1
* end :exec2 1
..after call :exec recurse 1
..recurse 2 done

I cannot explain the execution flow between these two lines.

Code: Select all

* before recurse 1
* begin :exec2 !rearg1! %

The only difference between :exec (which works) and :exec-fail (which goes wild) is that the former makes a temporary variable !exec! which it then executes, while the latter attempts to run the same command line directly.

Liviu

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: unexpected jump

#2 Post by dbenham » 26 Jan 2012 00:30

Both exec and exec-fail are "broken" :!:

You are invoking a batch from within another batch without a CALL statement. Forget for the moment that the batch is executing itself. Control is supposed to be transferred to the 2nd batch and it is not supposed to return to the original batch. BUT, you are invoking the 2nd batch after having made a CALL within the 1st, so it HAS to return. I don't think the language has a defined behavior for that.

I am getting slightly different behavior on Vista 64 then what you report on XP. But it is still equally as weird.

To make things easier to understand, I've simplified the situation by replacing the recursive nature with 2 batch files - part1 and part2.

part1.bat

Code: Select all

@echo off
setlocal disableDelayedExpansion
set var=value
cls
echo before direct
call :direct
echo after direct  var=!var!
echo(

echo before direct2
call :direct2
echo after direct2  var=!var!
echo(

echo before indirect
call :indirect
echo after indirect  var=!var!
exit /b

:indirect
setlocal enableDelayedExpansion
set cmd=part2
echo begin indirect  var=!var!
!cmd! arg1
echo end indirect NEVER REACHED
exit /b

:direct
setlocal enableDelayedExpansion
echo begin direct  var=!var!
part2 arg1
echo end direct NEVER REACHED
exit /b

:direct2
setlocal enableDelayedExpansion
echo begin direct2  var=!var!
part2 arg1
echo end direct2 NEVER REACHED
exit /b

part2.bat

Code: Select all

@echo off
echo part2 main      var=!var!  %%1=%1
exit /b

:indirect
echo part2 indirect  var=!var!  %%1=%1
exit /b

:direct
echo part2 direct  var=!var!  %%1=%1
exit /b

results after executing part1:

Code: Select all

before direct
begin direct  var=value
part2 direct  var=value  %1=arg1
after direct  var=!var!

before direct2
begin direct2  var=value
The system cannot find the batch label specified - direct2
after direct2  var=!var!

before indirect
begin indirect  var=value
part2 main      var=value  %1=arg1
after indirect  var=!var!

In all cases, none of the part1 subroutines finish - they are truncated once part 2 is called. In all cases, the part1 subroutines do return to the main part1 program.

The interesting bit is what happens when part2 is invoked.

When part2 is invoked directly from within the part1 :direct routine, control is immediately transferred to the :direct label in part2 - the top of the script is ignored :!: :shock:

When part2 is invoked directly from within the part1 :direct2 routine, part2 fails entirely because it is looking for a :direct2 label and fails to find it :!:

When part2 is invoked indirectly via delayed expansion from within part1 :indirect, it executes normally from the top. The :indirect label is ignored.

The extra output concerning var=value and arg1 is just to prove that the SETLOCAL is being terminated properly when the subroutines return, and also that the arguments are passed to part2 properly.

I find this very interesting. I wonder if jeb has seen this before, or if there is a use for this weird behavior.

Dave Benham

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: unexpected jump

#3 Post by Liviu » 26 Jan 2012 16:35

dbenham wrote:Both exec and exec-fail are "broken" :!:

For different values of "broken", though ;-)

Your example verifies that the :indirect call (my :exec) does indeed work in the sense that I meant in the original post - execute an external program without an explicit "call" or "cmd /c" which cause a reparse of the command line.

Now, your :direct call (my :exec-fail) probably explains the execution flow oddity that I noted, yet it's not entirely clear to me how the parameters got shifted there. But you're right, the recursion likely added another layer of complexity on top of what's a very strange behavior already.

That said, this :direct behavior (if verified/confirmed/portable/etc) could be a very useful trick on its own. It basically amounts to being able to call individual labels inside an external batch file. Haven't measured and won't comment on performance, but such a feature could come in quite handy for "library" purposes. For example, it would allow code like...

Code: Select all

set x=%~1
call :mathlib.multiply %x% 2 x
call :mathlib.increment %x% x
echo 2 * %~1 + 1 = %x%
goto :eof

@rem the following assumes that mathlib.cmd exists and has
@rem :mathlib.multiply, :mathlib.increment labels which do the obvious
:mathlib.multiply
:mathlib.increment
mathlib %*
echo this line NEVER reached
goto :eof

Liviu

jeb
Expert
Posts: 1041
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: unexpected jump

#4 Post by jeb » 27 Jan 2012 03:28

I love new behaviour :)

I tested it with Win7 x64 and it "works" (implicit jump is active)

The implicit jump is strange :!:

But I can't imagine how bad the code must be in cmd.exe that this can occour.
Even the strangest anti-pattern shouldn't produce this :?:

Btw. I suppose this is a goto not a call, as in %0 is the name of the batch file, not the name of the function

part1.bat

Code: Select all

@echo off

call :testLabel
echo part1 end
exit /b

:testLabel
echo part1 testlabel %%0 = %0
part2 argFromPart1
echo Never reached
exit /b


part2.bat

Code: Select all

@echo off

echo %0 main
exit /b

:testLabel
echo part2:testLabel %%0=%0 %%1=%1
exit /b


jeb

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: unexpected jump

#5 Post by Liviu » 29 Jan 2012 01:27

jeb wrote:But I can't imagine how bad the code must be in cmd.exe that this can occour.

I was wondering the same... My imagination cannot fathom a scenario where this could be the side effect of even the weirdest implementation bug. Alternative theory might then be that it's a "hidden feature" for whatever purposes e.g. to somehow aid in-house batch file debugging. However, I find that theory unsatisfactory, too. So, bottom line, I am just as mystified as you are.

Liviu

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: unexpected jump

#6 Post by Liviu » 29 Jan 2012 21:13

jeb wrote:Btw. I suppose this is a goto not a call, as in %0 is the name of the batch file, not the name of the function

Right. However, it seems to be very "syntactically sensitive". If, in part1, you replace the "part2 argFromPart1" line with "if 0==0 part2 argFromPart1" then the part2 execution will start at the top, instead. And, in all cases, replacing "exit /b" with just "exit" will close the original part1 console itself.

Liviu

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: unexpected jump

#7 Post by dbenham » 29 Jan 2012 22:15

:shock:
Stop finding these ridiculous cases - my brain hurts :lol:

It seems that the implicit jump fails to "work" if there is anything other than command line token delimiters before part2. (ie <space> <tab> ; , = )

All of the following start at the top

Code: Select all

echo hello & part2 ...
verify >nul & part2 ...
(part2 ...)

Even this starts at the top

Code: Select all

(
  part2 ...
)


Liviu wrote:And, in all cases, replacing "exit /b" with just "exit" will close the original part1 console itself.
Nothing unusual there. That is how EXIT always works, hence the need for the /B option.


Dave Benham

alan_b
Expert
Posts: 357
Joined: 04 Oct 2008 09:49

Re: unexpected jump

#8 Post by alan_b » 30 Jan 2012 03:41

Liviu wrote:
jeb wrote:But I can't imagine how bad the code must be in cmd.exe that this can occour.

Alternative theory might then be that it's a "hidden feature" for whatever purposes e.g. to somehow aid in-house batch file debugging. However, I find that theory unsatisfactory, too. So, bottom line, I am just as mystified as you are.
Liviu

I totally absolutely disagree - it could be a deliberate assist for in-house purposes.

Some years ago it was either an Emergency Out of Band or a Patch Tuesday Security update that "updated" the heart of Windows.
I am fairly certain it was Explorer.exe.
Strange thing is that the new versions was :-
Significantly smaller ;
had identical version strings and creation / modification dates.

When I developed software using the 'C' language, a simple Header.H file determined whether DEBUG=TRUE or DEBUG=FALSE.
During development the DEBUG code was active and wasted 90% of the CPU cycles to ensure that only 10% was needed by active code,
and there were many supplementary run-time checks to facilitate testing and validate all assumptions,
and a flag on display to ensure instant recognition if DEBUG code ever got into equipment for the customer.

I never failed to cancel DEBUG=TRUE when releasing code to production.

It was instantly recognizable to me that regardless of whether Microsoft use 'C' or something else,
they had issued Explorer.exe with some form of "DEBUG" incorporated,
and the latest exploit was utilizing their "DEBUG" code,
hence Security update merely cancelled the "DEBUG" mode and thus stripped out the malware target and gave a SMALLER executable,
but the software library was totally unaltered - no other files were changed, no version strings were updated.

CMD.EXE starts with what COMMAND.COM did, does not quite implement exactly what it did, and throws in a great big bundle of extras.
It obviously needed debugging and I doubt that I am the only genius to use a tiny change in one file to supplement the intended product with extra DEBUG goodies.

30 years ago my software ran 24 hours a day 365 days a year giving non-stop security protection to military installations.
I never risked releasing DEBUG code

DOS and Windows were happy to give BSOD's every day and swear at users for not "shutting down properly.
The probably bundle DEBUG into everything they do and simply flip the debug switch when malware targets that aspect of Windows.

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: unexpected jump

#9 Post by Liviu » 30 Jan 2012 17:46

dbenham wrote:
Liviu wrote:And, in all cases, replacing "exit /b" with just "exit" will close the original part1 console itself.
Nothing unusual there. That is how EXIT always works, hence the need for the /B option.

Right, of course. Yet "always" is relative to the charted territories. The behavior in this case was odd enough that I honestly didn't know what to expect, even from trusted straightforward commands like EXIT ;-)

alan_b wrote:
Liviu wrote:
jeb wrote:But I can't imagine how bad the code must be in cmd.exe that this can occour.

Alternative theory might then be that it's a "hidden feature" for whatever purposes e.g. to somehow aid in-house batch file debugging. However, I find that theory unsatisfactory, too. So, bottom line, I am just as mystified as you are.
Liviu

I totally absolutely disagree - it could be a deliberate assist for in-house purposes.

I tend to agree more now, after the latest findings that even slight/cosmetic syntax changes cause the behavior to revert to the expected default. This could indeed be a sign of some deliberate "easter egg", be it intended for debugging or other purposes. That helps support with calls like: "oh, you stumbled upon our little secret feature we thought no sane user would find, but just throw a pair of paranthesis around the line, and it will work again as advertised".

Liviu

Post Reply