Sooner or later every shop that uses service programs sees this in a job log:
MCH4431 Program signature violation. Cause . . . : The source program SIGCALL specifies a signature X'00000000000000000000000CDE436702' which is not supported by service program SIGSRV. Recovery . : The service program interface has changed. Re-bind source program SIGCALL. The recovery text says "re-bind", and that is usually what people do: recompile the program that failed, then the next one, then the next one. It works, but it treats the symptom. The cause is how the service program was built.
To see exactly what happens, I ran five small experiments on PUB400 (IBM i 7.5): one service program SIGSRV, one caller SIGCALL, and a data area where the caller writes what it got back. Every result below is copied from those runs.
The setup
The service program starts with two procedures. Each one returns its own letter, so it is easy to see which procedure actually ran:
ctl-opt nomain; dcl-proc SigA export; dcl-pi *n char(10) end-pi; return 'A'; end-proc; dcl-proc SigB export; dcl-pi *n char(10) end-pi; return 'B'; end-proc; The caller calls both and writes the answers to a data area:
dcl-pr SigA char(10) end-pr; dcl-pr SigB char(10) end-pr; dcl-s outTxt char(40) dtaara('SIGOUT'); in *lock outTxt; outTxt = 'SigA=' + %trim(SigA()) + ' SigB=' + %trim(SigB()); out outTxt; *inlr = *on; return; Built the usual way:
CRTRPGMOD MODULE(QTEMP/SIGSRV) SRCFILE(MYLIB/QRPGLESRC) SRCMBR(SIGSRV) CRTSRVPGM SRVPGM(MYLIB/SIGSRV) MODULE(QTEMP/SIGSRV) EXPORT(*ALL) CRTRPGMOD MODULE(QTEMP/SIGCALL) SRCFILE(MYLIB/QRPGLESRC) SRCMBR(SIGCALL) CRTPGM PGM(MYLIB/SIGCALL) MODULE(QTEMP/SIGCALL) + BNDSRVPGM((MYLIB/SIGSRV)) ACTGRP(*NEW) Result: SigA=A SigB=B. So far, nothing special.
What a signature is
When CRTPGM binds SIGCALL to SIGSRV, it stores two things in the program:
- the signature of the service program at that moment (16 bytes), and
- for every procedure it uses, the position of that procedure in the export list, not its name.
At activation time the system checks the stored signature against the list of signatures the service program supports. If it is not in the list, you get MCH4431 before a single line of your program runs.
Both values can be read with SQL. The caller side is in QSYS2.BOUND_SRVPGM_INFO, and the service program side in QSYS2.PROGRAM_INFO:
select hex(bound_service_program_signature) from qsys2.bound_srvpgm_info where program_library = 'MYLIB' and program_name = 'SIGCALL'; -- 00000000000000000000000CDE436702 select signatures, hex(substr(export_signatures, 1, 16)) from qsys2.program_info where program_library = 'MYLIB' and program_name = 'SIGSRV' and object_type = '*SRVPGM'; -- 1 00000000000000000000000CDE436702 With EXPORT(*ALL) the signature is generated from the export list. The two values match, so the call works.
Experiment 1: add a procedure, keep EXPORT(*ALL)
A new requirement arrives, so SigC is added at the end of the source and the service program is rebuilt the same way, EXPORT(*ALL). SIGCALL is not touched. It does not even use SigC.
SIGSRV signature now 0000000000000000000000CDA7FD3902 SIGCALL still expects 00000000000000000000000CDE436702 CALL SIGCALL -> MCH4431 Program signature violation. This is the most common way to meet MCH4431. With EXPORT(*ALL), every new procedure changes the signature, so every program bound to the service program has to be rebound, including the ones that will never call the new procedure.
Experiment 2: binder source with a *PRV block
The fix is to stop letting the system guess and write the export list yourself in binder source:
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('SIGSRV V2') EXPORT SYMBOL(SIGA) EXPORT SYMBOL(SIGB) EXPORT SYMBOL(SIGC) ENDPGMEXP STRPGMEXP PGMLVL(*PRV) EXPORT SYMBOL(SIGA) EXPORT SYMBOL(SIGB) ENDPGMEXP CRTSRVPGM SRVPGM(MYLIB/SIGSRV) MODULE(QTEMP/SIGSRV) + EXPORT(*SRCFILE) SRCFILE(MYLIB/QSRVSRC) SRCMBR(SIGSRV) The *PRV block has no SIGNATURE keyword, so the system generates one from its list, the same way EXPORT(*ALL) did for the old two-procedure version. The result:
SIGSRV signatures 2 current E2C9C7E2D9E540E5F240404040404040 ('SIGSRV V2' in EBCDIC) previous 00000000000000000000000CDE436702 SIGCALL still expects 00000000000000000000000CDE436702 CALL SIGCALL -> SigA=A SigB=B The generated signature of the old list is exactly the one the existing caller stored. SIGCALL runs again, without being recompiled or rebound. This is a useful rescue when a service program was built with EXPORT(*ALL) for years: describe the old export list in a *PRV block, in its original order, and every existing caller keeps working.
Experiment 3: a fixed signature, new procedure at the end
Now SIGCALL is rebound once, so it stores 'SIGSRV V2'. Then SigD is added, and the binder source only gets one new line, at the end. The signature stays the same:
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('SIGSRV V2') EXPORT SYMBOL(SIGA) EXPORT SYMBOL(SIGB) EXPORT SYMBOL(SIGC) EXPORT SYMBOL(SIGD) ENDPGMEXP CALL SIGCALL -> SigA=A SigB=B No rebind, no *PRV block, no error. Positions 1 and 2 are still SigA and SigB, and the caller only knows positions. This is the pattern most shops settle on: a hard-coded signature, and new exports only ever added at the end.
Experiment 4: reorder the exports, keep the signature
This is the one that worries me. Someone tidies up the binder source and puts the exports in a different order. The signature literal stays 'SIGSRV V2':
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('SIGSRV V2') EXPORT SYMBOL(SIGB) EXPORT SYMBOL(SIGA) EXPORT SYMBOL(SIGC) EXPORT SYMBOL(SIGD) ENDPGMEXP CALL SIGCALL -> SigA=B SigB=A No MCH4431, no message in the job log, no error at all. The signature still matches, so the system trusts the caller. The caller asks for "export number 1", which is now SigB. The program calls the wrong procedure and carries on.
In this demo both procedures have the same interface, so it only returns a wrong letter. In a real service program the swapped procedures usually have different parameters. Then you get a wrong amount, a decimal data error far away from the cause, or a pointer error that nobody can explain, and only in the programs that were bound before the change.
A fixed signature is a promise: "the first N exports are still the same procedures in the same order." The system cannot check that promise for you. That is the trade-off you accept when you stop using *GEN.
The same thing happens if a procedure is removed from the middle of the list: every procedure after it moves up one position.
Finding the programs that will fail before they fail
Because both signatures are visible through SQL, you can check every caller before a release instead of waiting for MCH4431 in production:
select b.program_library, b.program_name, hex(b.bound_service_program_signature) as caller_signature, case when locate(cast(b.bound_service_program_signature as blob(16)), s.export_signatures) > 0 then 'OK' else 'WILL FAIL MCH4431' end as status from qsys2.bound_srvpgm_info b join qsys2.program_info s on s.program_library = b.bound_service_program_library and s.program_name = b.bound_service_program and s.object_type = '*SRVPGM' where b.bound_service_program_library = 'MYLIB' and b.bound_service_program = 'SIGSRV'; After experiment 1 it returned SIGCALL ... WILL FAIL MCH4431. After experiment 3 it returned OK. Two notes: BOUND_SRVPGM_INFO reads program objects, so on a large system filter by program_library as well. And it says OK after experiment 4 too, because the signature matches. The query finds the loud failures, not the silent one.
The rules I follow
- Use binder source, not
EXPORT(*ALL), for any service program that has more than one caller. - Add new exports at the end only. Never insert in the middle, never reorder, never delete. A procedure you no longer want stays in the list and can return an error.
- Choose one signature style per service program. Either a hard-coded signature that never changes (simple, but the order is your responsibility), or a new
*CURRENTsignature per release with every older list kept as*PRV(the system checks more for you, and the binder source grows over time). - If you inherit a service program built with
EXPORT(*ALL), describe its current export list in a*PRVblock in the same order before you add anything. The existing callers keep their generated signature. - Before a release, run the query above for every service program that changed.
- When you see
MCH4431, do not only rebind the program that failed. Find out why the signature changed, because every other caller has the same problem.
All results in this article come from a scripted run on PUB400: create the service program, bind the caller, change one thing, call, and read the data area and QSYS2 views after each step. If your shop handles signatures differently, for example one signature per release with a growing list of *PRV blocks, I would like to hear how that works for you after a few years.

